diff --git a/addon/authenticators/oidc.js b/addon/authenticators/oidc.js index b1eb5ded..580b4564 100644 --- a/addon/authenticators/oidc.js +++ b/addon/authenticators/oidc.js @@ -62,7 +62,7 @@ export default class OidcAuthenticator extends BaseAuthenticator { * @param {String} options.code The authentication code * @returns {Object} The parsed response data */ - async authenticate({ code, redirectUri, codeVerifier, isRefresh }) { + async authenticate(options) { if (!this.hasEndpointsConfigured) { await this._fetchAuthConfiguration.perform(); @@ -73,27 +73,19 @@ export default class OidcAuthenticator extends BaseAuthenticator { } } + const { isRefresh = false, redirectUri, customParams = {} } = options; + if (isRefresh) { + const DEFAULT_RETRY_COUNT = 0; return await this._refresh( this.session.data.authenticated.refresh_token, redirectUri, + DEFAULT_RETRY_COUNT, + customParams, ); } - const bodyObject = { - code, - client_id: this.configuration.clientId, - grant_type: "authorization_code", - redirect_uri: redirectUri, - }; - - if (this.configuration.enablePkce) { - bodyObject.code_verifier = codeVerifier; - } - - const body = Object.keys(bodyObject) - .map((k) => `${k}=${encodeURIComponent(bodyObject[k])}`) - .join("&"); + const body = this._buildBodyQuery(options); const response = await fetch( getAbsoluteUrl(this.configuration.tokenEndpoint, this.config.host), @@ -201,18 +193,20 @@ export default class OidcAuthenticator extends BaseAuthenticator { * @param {String} refresh_token The refresh token * @returns {Object} The parsed response data */ - async _refresh(refresh_token, redirectUri, retryCount = 0) { + async _refresh( + refresh_token, + redirectUri, + retryCount = 0, + customParams = {}, + ) { let isServerError = false; try { - const bodyObject = { + const body = this._buildBodyQuery({ + redirectUri, refresh_token, - client_id: this.configuration.clientId, - grant_type: "refresh_token", - redirect_uri: redirectUri, - }; - const body = Object.keys(bodyObject) - .map((k) => `${k}=${encodeURIComponent(bodyObject[k])}`) - .join("&"); + isRefresh: true, + customParams, + }); const response = await fetch( getAbsoluteUrl(this.configuration.tokenEndpoint, this.config.host), @@ -316,4 +310,43 @@ export default class OidcAuthenticator extends BaseAuthenticator { redirectUri, }); } + + /** + * Builds query parameters string for the authorize or refresh request + * + * @param {*} options + * @returns string + */ + _buildBodyQuery({ + code, + redirectUri, + codeVerifier, + isRefresh = false, + refresh_token, + customParams = {}, + }) { + const bodyObject = { + redirect_uri: redirectUri, + client_id: this.configuration.clientId, + grant_type: isRefresh ? "refresh_token" : "authorization_code", + ...customParams, + }; + + if (!isRefresh && code) { + bodyObject.code = code; + if (this.configuration.enablePkce) { + bodyObject.code_verifier = codeVerifier; + } + } + + if (isRefresh && refresh_token) { + bodyObject.refresh_token = refresh_token; + } + + const bodyQuery = Object.keys(bodyObject) + .map((k) => `${k}=${encodeURIComponent(bodyObject[k])}`) + .join("&"); + + return bodyQuery; + } } diff --git a/addon/routes/oidc-authentication.js b/addon/routes/oidc-authentication.js index 0e6e4729..d3fee11e 100644 --- a/addon/routes/oidc-authentication.js +++ b/addon/routes/oidc-authentication.js @@ -64,8 +64,9 @@ export default class OIDCAuthenticationRoute extends Route { * @param {Object} transition.to.queryParams The query params of the transition * @param {String} transition.to.queryParams.code The authentication code given by the identity provider * @param {String} transition.to.queryParams.state The state given by the identity provider + * @param {Object} customParams Custom query params to be added to the redirect URL */ - async afterModel(_, transition) { + async afterModel(_, transition, customParams = {}) { if (!this.config.authEndpoint) { throw new Error( "Please define all OIDC endpoints (auth, token, logout, userinfo)", @@ -84,7 +85,7 @@ export default class OIDCAuthenticationRoute extends Route { ); } - return this._handleRedirectRequest(queryParams); + return this._handleRedirectRequest(queryParams, customParams); } /** @@ -130,7 +131,7 @@ export default class OIDCAuthenticationRoute extends Route { * match this state, otherwise the authentication will fail to prevent from * CSRF attacks. */ - _handleRedirectRequest(queryParams) { + _handleRedirectRequest(queryParams, customParams = {}) { const state = v4(); // Store state to session data @@ -155,6 +156,7 @@ export default class OIDCAuthenticationRoute extends Route { `state=${state}`, `scope=${this.config.scope}`, queryParams[key] ? `${key}=${queryParams[key]}` : null, + new URLSearchParams(customParams).toString(), ]; if (this.config.enablePkce) { diff --git a/tests/unit/authenticators/oidc-test.js b/tests/unit/authenticators/oidc-test.js index f587595d..bf86c2c6 100644 --- a/tests/unit/authenticators/oidc-test.js +++ b/tests/unit/authenticators/oidc-test.js @@ -77,4 +77,29 @@ module("Unit | Authenticator | OIDC", function (hooks) { subject.singleLogout("myIdToken"); }); + + test("it supports sending custom parameters", function (assert) { + const bodyOptions = { + code: "test-code", + codeVerifier: "test-verifier", + redirectUri: "test/redirect", + isRefresh: true, + refresh_token: "test-refresh-token", + customParams: { foo: "bar" }, + }; + + const subject = this.owner.lookup("authenticator:oidc"); + const bodyWithRefresh = subject._buildBodyQuery(bodyOptions); + assert.strictEqual( + bodyWithRefresh, + "redirect_uri=test%2Fredirect&client_id=test-client&grant_type=refresh_token&foo=bar&refresh_token=test-refresh-token", + ); + + bodyOptions.isRefresh = false; + const bodyWithoutRefresh = subject._buildBodyQuery(bodyOptions); + assert.strictEqual( + bodyWithoutRefresh, + "redirect_uri=test%2Fredirect&client_id=test-client&grant_type=authorization_code&foo=bar&code=test-code", + ); + }); });