Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: autodiscover provider configuration #872

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 63 additions & 20 deletions addon/authenticators/oidc.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { later } from "@ember/runloop";
import { inject as service } from "@ember/service";
import { camelize } from "@ember/string";
import { lastValue, task } from "ember-concurrency";
import {
isServerErrorResponse,
isAbortError,
Expand All @@ -13,12 +15,47 @@ import { TrackedObject } from "tracked-built-ins";
import config from "ember-simple-auth-oidc/config";
import getAbsoluteUrl from "ember-simple-auth-oidc/utils/absolute-url";

const camelizeObjectKeys = (obj) => {
Object.keys(obj).forEach((key) => {
obj[camelize(key)] = obj[key];
delete obj[key];
});
return obj;
};

export default class OidcAuthenticator extends BaseAuthenticator {
@service router;
@service session;

@config config;

get configuration() {
return { ...this.config, ...this.fetchedConfig };
}

get hasEndpointsConfigured() {
return (
this.configuration.tokenEndpoint && this.configuration.userinfoEndpoint
);
}

/**
* Tries to fetch the OIDC provider configuration from the specified host/realm.
* SPEC: https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig
*/
@lastValue("_fetchAuthConfiguration") fetchedConfig;
_fetchAuthConfiguration = task(async () => {
if (!this.config.host) {
throw new Error("Please define a OIDC host.");
}
const response = await fetch(
`${this.config.host}/.well-known/openid-configuration`,
);
const _json = await response.json();

return camelizeObjectKeys(_json.data);
});

/**
* Authenticate the client with the given authentication code. The
* authentication call will return an access and refresh token which will
Expand All @@ -29,10 +66,14 @@ export default class OidcAuthenticator extends BaseAuthenticator {
* @returns {Object} The parsed response data
*/
async authenticate({ code, redirectUri, codeVerifier, isRefresh }) {
if (!this.config.tokenEndpoint || !this.config.userinfoEndpoint) {
throw new Error(
"Please define all OIDC endpoints (auth, token, userinfo)",
);
if (!this.hasEndpointsConfigured) {
await this._fetchAuthConfiguration.perform();

if (!this.hasEndpointsConfigured) {
throw new Error(
"Please define all OIDC endpoints (auth, token, userinfo)",
);
}
}

if (isRefresh) {
Expand All @@ -44,12 +85,12 @@ export default class OidcAuthenticator extends BaseAuthenticator {

const bodyObject = {
code,
client_id: this.config.clientId,
client_id: this.configuration.clientId,
grant_type: "authorization_code",
redirect_uri: redirectUri,
};

if (this.config.enablePkce) {
if (this.configuration.enablePkce) {
bodyObject.code_verifier = codeVerifier;
}

Expand All @@ -58,7 +99,7 @@ export default class OidcAuthenticator extends BaseAuthenticator {
.join("&");

const response = await fetch(
getAbsoluteUrl(this.config.tokenEndpoint, this.config.host),
getAbsoluteUrl(this.configuration.tokenEndpoint, this.config.host),
{
method: "POST",
headers: {
Expand Down Expand Up @@ -102,16 +143,16 @@ export default class OidcAuthenticator extends BaseAuthenticator {
* @param {String} idToken The id_token of the session to invalidate
*/
singleLogout(idToken) {
if (!this.config.endSessionEndpoint) {
if (!this.configuration.endSessionEndpoint) {
return;
}

const params = [];

if (this.config.afterLogoutUri) {
if (this.configuration.afterLogoutUri) {
params.push(
`post_logout_redirect_uri=${getAbsoluteUrl(
this.config.afterLogoutUri,
this.configuration.afterLogoutUri,
)}`,
);
}
Expand All @@ -122,8 +163,8 @@ export default class OidcAuthenticator extends BaseAuthenticator {

this._redirectToUrl(
`${getAbsoluteUrl(
this.config.endSessionEndpoint,
this.config.host,
this.configuration.endSessionEndpoint,
this.configuration.host,
)}?${params.join("&")}`,
);
}
Expand Down Expand Up @@ -168,7 +209,7 @@ export default class OidcAuthenticator extends BaseAuthenticator {
try {
const bodyObject = {
refresh_token,
client_id: this.config.clientId,
client_id: this.configuration.clientId,
grant_type: "refresh_token",
redirect_uri: redirectUri,
};
Expand All @@ -177,7 +218,7 @@ export default class OidcAuthenticator extends BaseAuthenticator {
.join("&");

const response = await fetch(
getAbsoluteUrl(this.config.tokenEndpoint, this.config.host),
getAbsoluteUrl(this.configuration.tokenEndpoint, this.config.host),
{
method: "POST",
headers: {
Expand All @@ -203,7 +244,7 @@ export default class OidcAuthenticator extends BaseAuthenticator {
} catch (e) {
if (
(isServerError || isAbortError(e)) &&
retryCount < this.config.amountOfRetries - 1
retryCount < this.configuration.amountOfRetries - 1
) {
return new Promise((resolve) => {
later(
Expand All @@ -212,7 +253,7 @@ export default class OidcAuthenticator extends BaseAuthenticator {
resolve(
this._refresh(refresh_token, redirectUri, retryCount + 1),
),
this.config.retryTimeout,
this.configuration.retryTimeout,
);
});
}
Expand All @@ -228,10 +269,10 @@ export default class OidcAuthenticator extends BaseAuthenticator {
*/
async _getUserinfo(accessToken) {
const response = await fetch(
getAbsoluteUrl(this.config.userinfoEndpoint, this.config.host),
getAbsoluteUrl(this.configuration.userinfoEndpoint, this.config.host),
{
headers: {
Authorization: `${this.config.authPrefix} ${accessToken}`,
Authorization: `${this.configuration.authPrefix} ${accessToken}`,
Accept: "application/json",
},
},
Expand Down Expand Up @@ -263,9 +304,11 @@ export default class OidcAuthenticator extends BaseAuthenticator {

const expireInMilliseconds = expires_in
? expires_in * 1000
: this.config.expiresIn;
: this.configuration.expiresIn;
const expireTime =
new Date().getTime() + expireInMilliseconds - this.config.refreshLeeway;
new Date().getTime() +
expireInMilliseconds -
this.configuration.refreshLeeway;

return new TrackedObject({
access_token,
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"@apollo/client": "^3.11.1",
"@babel/core": "^7.25.2",
"@embroider/macros": "^1.16.9",
"@ember/string": "4.0.0",
"base64-js": "^1.5.1",
"ember-auto-import": "^2.8.1",
"ember-cli-babel": "^8.2.0",
Expand All @@ -50,7 +51,6 @@
"@commitlint/cli": "19.5.0",
"@commitlint/config-conventional": "19.5.0",
"@ember/optional-features": "2.1.0",
"@ember/string": "4.0.0",
"@ember/test-helpers": "4.0.4",
"@embroider/test-setup": "4.0.0",
"@glimmer/tracking": "1.1.2",
Expand Down
Loading