Skip to content

Commit

Permalink
Allow lazy config in Next.js layer
Browse files Browse the repository at this point in the history
  • Loading branch information
adamjmcgrath committed Nov 7, 2023
1 parent 30c100e commit d79ff8f
Show file tree
Hide file tree
Showing 37 changed files with 612 additions and 963 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@
"!<rootDir>/src/edge.ts",
"!<rootDir>/src/index.ts",
"!<rootDir>/src/shared.ts",
"!<rootDir>/src/version.ts",
"!<rootDir>/src/auth0-session/config.ts",
"!<rootDir>/src/auth0-session/index.ts",
"!<rootDir>/src/auth0-session/session-cache.ts"
Expand Down
8 changes: 3 additions & 5 deletions src/auth0-session/client/abstract-client.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Config, GetConfig } from '../config';
import { Auth0Request } from '../http';
import { Config } from '../config';

export type Telemetry = {
name: string;
Expand Down Expand Up @@ -85,10 +85,6 @@ export interface AuthorizationParameters {
}

export abstract class AbstractClient {
protected getConfig: () => Config | Promise<Config>;
constructor(getConfig: GetConfig, protected telemetry: Telemetry) {
this.getConfig = typeof getConfig === 'function' ? getConfig : () => getConfig;
}
abstract authorizationUrl(parameters: Record<string, unknown>): Promise<string>;
abstract callbackParams(req: Auth0Request, expectedState: string): Promise<URLSearchParams>;
abstract callback(
Expand All @@ -107,3 +103,5 @@ export abstract class AbstractClient {
abstract generateRandomNonce(): string;
abstract calculateCodeChallenge(codeVerifier: string): Promise<string> | string;
}

export type GetClient = (config: Config) => Promise<AbstractClient>;
172 changes: 86 additions & 86 deletions src/auth0-session/client/edge-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@ import {
OpenIDCallbackChecks,
TokenEndpointResponse,
AbstractClient,
EndSessionParameters
EndSessionParameters,
Telemetry
} from './abstract-client';
import { ApplicationError, DiscoveryError, IdentityProviderError, UserInfoError } from '../utils/errors';
import { AccessTokenError, AccessTokenErrorCode } from '../../utils/errors';
import urlJoin from 'url-join';
import { Config } from '../config';

const encodeBase64 = (input: string) => {
const unencoded = new TextEncoder().encode(input);
Expand All @@ -24,68 +26,18 @@ const encodeBase64 = (input: string) => {
};

export class EdgeClient extends AbstractClient {
private client?: oauth.Client;
private as?: oauth.AuthorizationServer;

private async httpOptions(): Promise<oauth.HttpRequestOptions> {
const headers = new Headers();
const config = await this.getConfig();
if (config.enableTelemetry) {
const { name, version } = this.telemetry;
headers.set('User-Agent', `${name}/${version}`);
headers.set(
'Auth0-Client',
encodeBase64(
JSON.stringify({
name,
version,
env: {
edge: true
}
})
)
);
}
return {
signal: AbortSignal.timeout(config.httpTimeout),
headers
};
}

private async getClient(): Promise<[oauth.AuthorizationServer, oauth.Client]> {
if (this.as) {
return [this.as, this.client as oauth.Client];
}
const config = await this.getConfig();
if (config.authorizationParams.response_type !== 'code') {
throw new Error('This SDK only supports `response_type=code` when used in an Edge runtime.');
}

const issuer = new URL(config.issuerBaseURL);
try {
this.as = await oauth
.discoveryRequest(issuer, await this.httpOptions())
.then((response) => oauth.processDiscoveryResponse(issuer, response));
} catch (e) {
throw new DiscoveryError(e, config.issuerBaseURL);
}

this.client = {
client_id: config.clientID,
...(!config.clientAssertionSigningKey && { client_secret: config.clientSecret }),
token_endpoint_auth_method: config.clientAuthMethod,
id_token_signed_response_alg: config.idTokenSigningAlg,
[oauth.clockTolerance]: config.clockTolerance
};

return [this.as, this.client];
constructor(
private client: oauth.Client,
private as: oauth.AuthorizationServer,
private config: Config,
private httpOptions: oauth.HttpRequestOptions
) {
super();
}

async authorizationUrl(parameters: Record<string, unknown>): Promise<string> {
const [as] = await this.getClient();
const config = await this.getConfig();
const authorizationUrl = new URL(as.authorization_endpoint as string);
authorizationUrl.searchParams.set('client_id', config.clientID);
const authorizationUrl = new URL(this.as.authorization_endpoint as string);
authorizationUrl.searchParams.set('client_id', this.config.clientID);
Object.entries(parameters).forEach(([key, value]) => {
if (value === null || value === undefined) {
return;
Expand All @@ -96,12 +48,11 @@ export class EdgeClient extends AbstractClient {
}

async callbackParams(req: Auth0Request, expectedState: string) {
const [as, client] = await this.getClient();
const url =
req.getMethod().toUpperCase() === 'GET' ? new URL(req.getUrl()) : new URLSearchParams(await req.getBody());
let result: ReturnType<typeof oauth.validateAuthResponse>;
try {
result = oauth.validateAuthResponse(as, client, url, expectedState);
result = oauth.validateAuthResponse(this.as, this.client, url, expectedState);
} catch (e) {
throw new ApplicationError(e);
}
Expand All @@ -121,30 +72,29 @@ export class EdgeClient extends AbstractClient {
checks: OpenIDCallbackChecks,
extras: CallbackExtras
): Promise<TokenEndpointResponse> {
const [as, client] = await this.getClient();
const { clientAssertionSigningKey, clientAssertionSigningAlg } = await this.getConfig();
const { clientAssertionSigningKey, clientAssertionSigningAlg } = this.config;

let clientPrivateKey = clientAssertionSigningKey as CryptoKey | undefined;
/* c8 ignore next 3 */
if (clientPrivateKey && !(clientPrivateKey instanceof CryptoKey)) {
clientPrivateKey = await jose.importPKCS8<CryptoKey>(clientPrivateKey, clientAssertionSigningAlg || 'RS256');
}
const response = await oauth.authorizationCodeGrantRequest(
as,
client,
this.as,
this.client,
parameters,
redirectUri,
checks.code_verifier as string,
{
additionalParameters: extras.exchangeBody,
...(clientPrivateKey && { clientPrivateKey }),
...this.httpOptions()
...this.httpOptions
}
);

const result = await oauth.processAuthorizationCodeOpenIDResponse(
as,
client,
this.as,
this.client,
response,
checks.nonce,
checks.max_age
Expand All @@ -160,18 +110,16 @@ export class EdgeClient extends AbstractClient {
}

async endSessionUrl(parameters: EndSessionParameters): Promise<string> {
const [as] = await this.getClient();
const issuerUrl = new URL(as.issuer);
const config = await this.getConfig();
const issuerUrl = new URL(this.as.issuer);

if (
config.idpLogout &&
(config.auth0Logout || (issuerUrl.hostname.match('\\.auth0\\.com$') && config.auth0Logout !== false))
this.config.idpLogout &&
(this.config.auth0Logout || (issuerUrl.hostname.match('\\.auth0\\.com$') && this.config.auth0Logout !== false))
) {
const { id_token_hint, post_logout_redirect_uri, ...extraParams } = parameters;
const auth0LogoutUrl: URL = new URL(urlJoin(as.issuer, '/v2/logout'));
const auth0LogoutUrl: URL = new URL(urlJoin(this.as.issuer, '/v2/logout'));
post_logout_redirect_uri && auth0LogoutUrl.searchParams.set('returnTo', post_logout_redirect_uri);
auth0LogoutUrl.searchParams.set('client_id', config.clientID);
auth0LogoutUrl.searchParams.set('client_id', this.config.clientID);
Object.entries(extraParams).forEach(([key, value]: [string, string]) => {
if (value === null || value === undefined) {
return;
Expand All @@ -180,39 +128,37 @@ export class EdgeClient extends AbstractClient {
});
return auth0LogoutUrl.toString();
}
if (!as.end_session_endpoint) {
if (!this.as.end_session_endpoint) {
throw new Error('RP Initiated Logout is not supported on your Authorization Server.');
}
const oidcLogoutUrl = new URL(as.end_session_endpoint);
const oidcLogoutUrl = new URL(this.as.end_session_endpoint);
Object.entries(parameters).forEach(([key, value]: [string, string]) => {
if (value === null || value === undefined) {
return;
}
oidcLogoutUrl.searchParams.set(key, value);
});

oidcLogoutUrl.searchParams.set('client_id', config.clientID);
oidcLogoutUrl.searchParams.set('client_id', this.config.clientID);
return oidcLogoutUrl.toString();
}

async userinfo(accessToken: string): Promise<Record<string, unknown>> {
const [as, client] = await this.getClient();
const response = await oauth.userInfoRequest(as, client, accessToken, await this.httpOptions());
const response = await oauth.userInfoRequest(this.as, this.client, accessToken, this.httpOptions);

try {
return await oauth.processUserInfoResponse(as, client, oauth.skipSubjectCheck, response);
return await oauth.processUserInfoResponse(this.as, this.client, oauth.skipSubjectCheck, response);
} catch (e) {
throw new UserInfoError(e.message);
}
}

async refresh(refreshToken: string, extras: { exchangeBody: Record<string, any> }): Promise<TokenEndpointResponse> {
const [as, client] = await this.getClient();
const res = await oauth.refreshTokenGrantRequest(as, client, refreshToken, {
const res = await oauth.refreshTokenGrantRequest(this.as, this.client, refreshToken, {
additionalParameters: extras.exchangeBody,
...this.httpOptions()
...this.httpOptions
});
const result = await oauth.processRefreshTokenResponse(as, client, res);
const result = await oauth.processRefreshTokenResponse(this.as, this.client, res);
if (oauth.isOAuth2Error(result)) {
throw new AccessTokenError(
AccessTokenErrorCode.FAILED_REFRESH_GRANT,
Expand All @@ -239,3 +185,57 @@ export class EdgeClient extends AbstractClient {
return oauth.calculatePKCECodeChallenge(codeVerifier);
}
}

export const clientGetter = (telemetry: Telemetry): ((config: Config) => Promise<EdgeClient>) => {
let client: EdgeClient;
return async (config) => {
if (!client) {
const headers = new Headers();
if (config.enableTelemetry) {
const { name, version } = telemetry;
headers.set('User-Agent', `${name}/${version}`);
headers.set(
'Auth0-Client',
encodeBase64(
JSON.stringify({
name,
version,
env: {
edge: true
}
})
)
);
}
const httpOptions: oauth.HttpRequestOptions = {
signal: AbortSignal.timeout(config.httpTimeout),
headers
};

if (config.authorizationParams.response_type !== 'code') {
throw new Error('This SDK only supports `response_type=code` when used in an Edge runtime.');
}

const issuer = new URL(config.issuerBaseURL);
let as: oauth.AuthorizationServer;
try {
as = await oauth
.discoveryRequest(issuer, httpOptions)
.then((response) => oauth.processDiscoveryResponse(issuer, response));
} catch (e) {
throw new DiscoveryError(e, config.issuerBaseURL);
}

const oauthClient: oauth.Client = {
client_id: config.clientID,
...(!config.clientAssertionSigningKey && { client_secret: config.clientSecret }),
token_endpoint_auth_method: config.clientAuthMethod,
id_token_signed_response_alg: config.idTokenSigningAlg,
[oauth.clockTolerance]: config.clockTolerance
};

client = new EdgeClient(oauthClient, as, config, httpOptions);
}
return client;
};
};
Loading

0 comments on commit d79ff8f

Please sign in to comment.