Repository: authts/oidc-client-ts (ts)

Issue:
RFC8707 - Possible Missing Multiple Resources handling for Code Flow Authorization Request
Hello,

My question might be totally naive, but here I go:
We have an SSO from which we're gettings Access/Refresh/Id token in order to later call 2 Services.
We're trying to add the `resource` information as part of the AuthCode Grant during signing in order to generate dedicated token(s ?) to hit both ApiOne and ApiTwo. (see image above from the RFC)
[RFC 8707 - Resource Indicators for OAuth 2.0](https://www.rfc-editor.org/rfc/rfc8707.html)
![image](https://user-images.githubusercontent.com/2266487/202176864-d9fc43a8-cc7e-483d-a4b0-07b207d22375.png)

(On refresh the same will apply by sending the last valid refresh token + the appropriate resources)

My question being ... how can this be achieved with the library today for both SignIn, Refresh, etc ...
From the `OidcClient.ts` `resource` seems to be a simple string here:
https://github.com/authts/oidc-client-ts/blob/0e6bfc22ba2181bf52934b1666ac61d9a0a92069/src/OidcClient.ts#L37

and it seems to be forwarded
https://github.com/authts/oidc-client-ts/blob/0e6bfc22ba2181bf52934b1666ac61d9a0a92069/src/OidcClient.ts#L111

And I cannot find the could mapping that to the `&resource=` 

Pull request description:
allow multiple resource parameters
<!-- Please link relevant issue numbers or provide context for this change -->
Closes/fixes #769

### Checklist

- [x] This PR makes changes to the public API <!-- was the API report (docs/oidc-client-ts.api.md) updated by this PR? -->
- [x] I have included links for closing relevant issue numbers


Patch:
diff --git a/docs/oidc-client-ts.api.md b/docs/oidc-client-ts.api.md
index 9551d2da..bd66c08d 100644
--- a/docs/oidc-client-ts.api.md
+++ b/docs/oidc-client-ts.api.md
@@ -76,7 +76,7 @@ export interface CreateSigninRequestArgs {
     // (undocumented)
     request_uri?: string;
     // (undocumented)
-    resource?: string;
+    resource?: string | string[];
     // (undocumented)
     response_mode?: "query" | "fragment";
     // (undocumented)
@@ -357,7 +357,7 @@ export interface OidcClientSettings {
     redirect_uri: string;
     // @deprecated (undocumented)
     refreshTokenCredentials?: "same-origin" | "include" | "omit";
-    resource?: string;
+    resource?: string | string[];
     response_mode?: "query" | "fragment";
     response_type?: string;
     revokeTokenAdditionalContentTypes?: string[];
@@ -414,7 +414,7 @@ export class OidcClientSettingsStore {
     // (undocumented)
     readonly redirect_uri: string;
     // (undocumented)
-    readonly resource: string | undefined;
+    readonly resource: string | string[] | undefined;
     // (undocumented)
     readonly response_mode: "query" | "fragment";
     // (undocumented)
@@ -615,7 +615,7 @@ export type SigninRedirectArgs = RedirectParams & ExtraSigninRequestArgs;
 
 // @public (undocumented)
 export class SigninRequest {
-    constructor({ url, authority, client_id, redirect_uri, response_type, scope, state_data, response_mode, request_type, client_secret, nonce, skipUserInfo, extraQueryParams, extraTokenParams, ...optionalParams }: SigninRequestArgs);
+    constructor({ url, authority, client_id, redirect_uri, response_type, scope, state_data, response_mode, request_type, client_secret, nonce, resource, skipUserInfo, extraQueryParams, extraTokenParams, ...optionalParams }: SigninRequestArgs);
     // (undocumented)
     readonly state: SigninState;
     // (undocumented)
@@ -657,7 +657,7 @@ export interface SigninRequestArgs {
     // (undocumented)
     request_uri?: string;
     // (undocumented)
-    resource?: string;
+    resource?: string | string[];
     // (undocumented)
     response_mode?: "query" | "fragment";
     // (undocumented)
diff --git a/src/OidcClient.ts b/src/OidcClient.ts
index ac7bd62b..031df5ae 100644
--- a/src/OidcClient.ts
+++ b/src/OidcClient.ts
@@ -34,7 +34,7 @@ export interface CreateSigninRequestArgs {
     id_token_hint?: string;
     login_hint?: string;
     acr_values?: string;
-    resource?: string;
+    resource?: string | string[];
     response_mode?: "query" | "fragment";
     request?: string;
     request_uri?: string;
diff --git a/src/OidcClientSettings.ts b/src/OidcClientSettings.ts
index df3773a4..733e2f50 100644
--- a/src/OidcClientSettings.ts
+++ b/src/OidcClientSettings.ts
@@ -66,7 +66,7 @@ export interface OidcClientSettings {
     /** optional protocol param */
     acr_values?: string;
     /** optional protocol param */
-    resource?: string;
+    resource?: string | string[];
 
     /** optional protocol param (default: "query") */
     response_mode?: "query" | "fragment";
@@ -149,7 +149,7 @@ export class OidcClientSettingsStore {
     public readonly max_age: number | undefined;
     public readonly ui_locales: string | undefined;
     public readonly acr_values: string | undefined;
-    public readonly resource: string | undefined;
+    public readonly resource: string | string[] | undefined;
     public readonly response_mode: "query" | "fragment";
 
     // behavior flags
diff --git a/src/SigninRequest.ts b/src/SigninRequest.ts
index 6a5c6052..04b3b7e4 100644
--- a/src/SigninRequest.ts
+++ b/src/SigninRequest.ts
@@ -24,7 +24,7 @@ export interface SigninRequestArgs {
     id_token_hint?: string;
     login_hint?: string;
     acr_values?: string;
-    resource?: string;
+    resource?: string | string[];
     response_mode?: "query" | "fragment" ;
     request?: string;
     request_uri?: string;
@@ -33,7 +33,7 @@ export interface SigninRequestArgs {
     client_secret?: string;
     extraTokenParams?: Record<string, unknown>;
     skipUserInfo?: boolean;
-    nonce?: string; 
+    nonce?: string;
 
     /** custom "state", which can be used by a caller to have "data" round tripped */
     state_data?: unknown;
@@ -53,6 +53,7 @@ export class SigninRequest {
         url, authority, client_id, redirect_uri, response_type, scope,
         // optional
         state_data, response_mode, request_type, client_secret, nonce,
+        resource,
         skipUserInfo,
         extraQueryParams,
         extraTokenParams,
@@ -108,6 +109,13 @@ export class SigninRequest {
             parsedUrl.searchParams.append("code_challenge_method", "S256");
         }
 
+        if (resource) {
+            // https://datatracker.ietf.org/doc/html/rfc8707
+            const resources = Array.isArray(resource) ? resource : [resource];
+            resources
+                .forEach(r => parsedUrl.searchParams.append("resource", r));
+        }
+
         for (const [key, value] of Object.entries({ response_mode, ...optionalParams, ...extraQueryParams })) {
             if (value != null) {
                 parsedUrl.searchParams.append(key, value.toString());


Test patch:
diff --git a/src/SigninRequest.test.ts b/src/SigninRequest.test.ts
index 273b8a08..b63e7c81 100644
--- a/src/SigninRequest.test.ts
+++ b/src/SigninRequest.test.ts
@@ -142,7 +142,7 @@ describe("SigninRequest", () => {
             expect(subject.url).toContain("acr_values=foo");
         });
 
-        it("should include resource", () => {
+        it("should include a resource", () => {
             // arrange
             settings.resource = "foo";
 
@@ -153,6 +153,17 @@ describe("SigninRequest", () => {
             expect(subject.url).toContain("resource=foo");
         });
 
+        it("should include multiple resources", () => {
+            // arrange
+            settings.resource = ["foo", "bar"];
+
+            // act
+            subject = new SigninRequest(settings);
+
+            // assert
+            expect(subject.url).toContain("resource=foo&resource=bar");
+        });
+
         it("should include response_mode", () => {
             // arrange
             settings.response_mode = "fragment";