Skip to content
This repository has been archived by the owner on Jan 10, 2024. It is now read-only.

Commit

Permalink
fix: authts#170 revert removing too much from commit "remove implicit…
Browse files Browse the repository at this point in the history
… flow" (320168b) and adapt
  • Loading branch information
pamapa committed Nov 2, 2021
1 parent 811ed0f commit 310b0f2
Show file tree
Hide file tree
Showing 26 changed files with 735 additions and 27 deletions.
3 changes: 1 addition & 2 deletions docs/migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,15 @@ Ported library from JavaScript to TypeScript.
- renamed staleStateAge to staleStateAgeInSeconds
- removed ResponseValidatorCtor and MetadataServiceCtor, if needed OidcClient/UserManager class must be extended
- changed response_type, only code flow (PKCE) is supported
- removed loadUserInfo

**UserManagerSettings:**
- renamed accessTokenExpiringNotificationTime to accessTokenExpiringNotificationTimeInSeconds
- changed silentRequestTimeout (milliseconds) to silentRequestTimeoutInSeconds
- changed checkSessionInterval (milliseconds) to checkSessionIntervalInSeconds
- default of automaticSilentRenew changed from false to true
- default of validateSubOnSilentRenew changed from false to true
- default of includeIdTokenInSilentRenew changed from true to false
- default of monitorSession changed from true to false
- removed includeIdTokenInSilentRenew

**UserManager:**
- signoutPopupCallback to pass optionally keepOpen as true, second argument must be used
21 changes: 15 additions & 6 deletions docs/oidc-client-ts.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ export interface CreateSigninRequestArgs {
// (undocumented)
extraTokenParams?: Record<string, any>;
// (undocumented)
id_token_hint?: string;
// (undocumented)
login_hint?: string;
// (undocumented)
max_age?: number;
Expand Down Expand Up @@ -174,11 +176,11 @@ export class OidcClient {
// Warning: (ae-forgotten-export) The symbol "SigninRequest" needs to be exported by the entry point index.d.ts
//
// (undocumented)
createSigninRequest({ response_type, scope, redirect_uri, state, prompt, display, max_age, ui_locales, login_hint, acr_values, resource, request, request_uri, response_mode, extraQueryParams, extraTokenParams, request_type, skipUserInfo }: CreateSigninRequestArgs): Promise<SigninRequest>;
createSigninRequest({ response_type, scope, redirect_uri, state, prompt, display, max_age, ui_locales, id_token_hint, login_hint, acr_values, resource, request, request_uri, response_mode, extraQueryParams, extraTokenParams, request_type, skipUserInfo }: CreateSigninRequestArgs): Promise<SigninRequest>;
// Warning: (ae-forgotten-export) The symbol "SignoutRequest" needs to be exported by the entry point index.d.ts
//
// (undocumented)
createSignoutRequest({ state, post_logout_redirect_uri, extraQueryParams, request_type }?: CreateSignoutRequestArgs): Promise<SignoutRequest>;
createSignoutRequest({ state, id_token_hint, post_logout_redirect_uri, extraQueryParams, request_type }?: CreateSignoutRequestArgs): Promise<SignoutRequest>;
// (undocumented)
readonly metadataService: MetadataService;
// (undocumented)
Expand Down Expand Up @@ -220,6 +222,7 @@ export interface OidcClientSettings {
// (undocumented)
extraTokenParams?: Record<string, any>;
filterProtocolClaims?: boolean;
loadUserInfo?: boolean;
// (undocumented)
max_age?: number;
// (undocumented)
Expand Down Expand Up @@ -318,6 +321,7 @@ export class TokenRevocationClient {
// @public (undocumented)
export class User {
constructor(args: {
id_token?: string;
session_state?: string;
access_token: string;
refresh_token?: string;
Expand All @@ -337,6 +341,8 @@ export class User {
set expires_in(value: number | undefined);
// (undocumented)
static fromStorageString(storageString: string): User;
// (undocumented)
id_token: string | undefined;
// Warning: (ae-forgotten-export) The symbol "UserProfile" needs to be exported by the entry point index.d.ts
//
// (undocumented)
Expand Down Expand Up @@ -464,6 +470,8 @@ export class UserManager {
protected _useRefreshToken(user: User): Promise<User>;
// (undocumented)
protected get _userStoreKey(): string;
// (undocumented)
protected _validateIdTokenFromTokenRefreshToken(profile: UserProfile, id_token: string): Promise<void>;
}

// @public (undocumented)
Expand Down Expand Up @@ -524,6 +532,7 @@ export interface UserManagerSettings extends OidcClientSettings {
accessTokenExpiringNotificationTimeInSeconds?: number;
automaticSilentRenew?: boolean;
checkSessionIntervalInSeconds?: number;
includeIdTokenInSilentRenew?: boolean;
// (undocumented)
monitorAnonymousSession?: boolean;
monitorSession?: boolean;
Expand Down Expand Up @@ -565,10 +574,10 @@ export class WebStorageStateStore implements StateStore {

// Warnings were encountered during analysis:
//
// src/OidcClient.ts:113:88 - (ae-forgotten-export) The symbol "SigninState" needs to be exported by the entry point index.d.ts
// src/OidcClient.ts:113:108 - (ae-forgotten-export) The symbol "SigninResponse" needs to be exported by the entry point index.d.ts
// src/OidcClient.ts:181:89 - (ae-forgotten-export) The symbol "State" needs to be exported by the entry point index.d.ts
// src/OidcClient.ts:181:115 - (ae-forgotten-export) The symbol "SignoutResponse" needs to be exported by the entry point index.d.ts
// src/OidcClient.ts:114:88 - (ae-forgotten-export) The symbol "SigninState" needs to be exported by the entry point index.d.ts
// src/OidcClient.ts:114:108 - (ae-forgotten-export) The symbol "SigninResponse" needs to be exported by the entry point index.d.ts
// src/OidcClient.ts:183:89 - (ae-forgotten-export) The symbol "State" needs to be exported by the entry point index.d.ts
// src/OidcClient.ts:183:115 - (ae-forgotten-export) The symbol "SignoutResponse" needs to be exported by the entry point index.d.ts

// (No @packageDocumentation comment for this package)

Expand Down
13 changes: 12 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@
"prepare": "husky install"
},
"dependencies": {
"crypto-js": "^4.1.1"
"crypto-js": "^4.1.1",
"jwt-decode": "^3.1.2"
},
"devDependencies": {
"@microsoft/api-extractor": "^7.18.10",
Expand Down
10 changes: 6 additions & 4 deletions src/OidcClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export interface CreateSigninRequestArgs {
display?: string;
max_age?: number;
ui_locales?: string;
id_token_hint?: string;
login_hint?: string;
acr_values?: string;
resource?: string;
Expand Down Expand Up @@ -64,7 +65,7 @@ export class OidcClient {
public async createSigninRequest({
response_type, scope, redirect_uri,
state,
prompt, display, max_age, ui_locales, login_hint, acr_values,
prompt, display, max_age, ui_locales, id_token_hint, login_hint, acr_values,
resource, request, request_uri, response_mode, extraQueryParams, extraTokenParams, request_type, skipUserInfo
}: CreateSigninRequestArgs): Promise<SigninRequest> {
Log.debug("OidcClient.createSigninRequest");
Expand All @@ -73,7 +74,7 @@ export class OidcClient {
scope = scope || this.settings.scope;
redirect_uri = redirect_uri || this.settings.redirect_uri;

// login_hint isn't allowed on _settings
// id_token_hint, login_hint aren't allowed on _settings
prompt = prompt || this.settings.prompt;
display = display || this.settings.display;
max_age = max_age || this.settings.max_age;
Expand All @@ -99,7 +100,7 @@ export class OidcClient {
response_type,
scope,
state_data: state,
prompt, display, max_age, ui_locales, login_hint, acr_values,
prompt, display, max_age, ui_locales, id_token_hint, login_hint, acr_values,
resource, request, request_uri, extraQueryParams, extraTokenParams, request_type, response_mode,
client_secret: this.settings.client_secret,
skipUserInfo
Expand Down Expand Up @@ -146,7 +147,7 @@ export class OidcClient {

public async createSignoutRequest({
state,
post_logout_redirect_uri, extraQueryParams, request_type
id_token_hint, post_logout_redirect_uri, extraQueryParams, request_type
}: CreateSignoutRequestArgs = {}): Promise<SignoutRequest> {
Log.debug("OidcClient.createSignoutRequest");

Expand All @@ -163,6 +164,7 @@ export class OidcClient {

const request = new SignoutRequest({
url,
id_token_hint,
post_logout_redirect_uri,
state_data: state,
extraQueryParams,
Expand Down
6 changes: 5 additions & 1 deletion src/OidcClientSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ export interface OidcClientSettings {

/** Should OIDC protocol claims be removed from profile (default: true) */
filterProtocolClaims?: boolean;
/** Flag to control if additional identity data is loaded from the user info endpoint in order to populate the user's profile (default: true) */
loadUserInfo?: boolean;
/** Number (in seconds) indicating the age of state entries in storage for authorize requests that are considered abandoned and thus can be cleaned up (default: 300) */
staleStateAgeInSeconds?: number;
/** The window of time (in seconds) to allow the current time to deviate when validating token's iat, nbf, and exp values (default: 300) */
Expand Down Expand Up @@ -95,6 +97,7 @@ export class OidcClientSettingsStore {

// behavior flags
public readonly filterProtocolClaims: boolean | undefined;
public readonly loadUserInfo: boolean | undefined;
public readonly staleStateAgeInSeconds: number;
public readonly clockSkewInSeconds: number;
public readonly userInfoJwtIssuer: "ANY" | "OP" | string | undefined;
Expand All @@ -116,7 +119,7 @@ export class OidcClientSettingsStore {
// optional protocol
prompt, display, max_age, ui_locales, acr_values, resource, response_mode,
// behavior flags
filterProtocolClaims = true,
filterProtocolClaims = true, loadUserInfo = true,
staleStateAgeInSeconds = DefaultStaleStateAgeInSeconds,
clockSkewInSeconds = DefaultClockSkewInSeconds,
userInfoJwtIssuer = "OP",
Expand Down Expand Up @@ -151,6 +154,7 @@ export class OidcClientSettingsStore {
this.response_mode = response_mode;

this.filterProtocolClaims = !!filterProtocolClaims;
this.loadUserInfo = !!loadUserInfo;
this.staleStateAgeInSeconds = staleStateAgeInSeconds;
this.clockSkewInSeconds = clockSkewInSeconds;
this.userInfoJwtIssuer = userInfoJwtIssuer;
Expand Down
78 changes: 77 additions & 1 deletion src/ResponseValidator.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.

import { Log } from "./utils";
import { Log, JwtUtils } from "./utils";
import type { MetadataService } from "./MetadataService";
import { UserInfoService } from "./UserInfoService";
import { TokenClient } from "./TokenClient";
import { ErrorResponse } from "./ErrorResponse";
import type { OidcClientSettingsStore } from "./OidcClientSettings";
Expand All @@ -17,11 +18,13 @@ const ProtocolClaims = ["at_hash", "iat", "nbf", "exp", "aud", "iss", "c_hash"];
export class ResponseValidator {
protected readonly _settings: OidcClientSettingsStore;
protected readonly _metadataService: MetadataService;
protected readonly _userInfoService: UserInfoService;
protected readonly _tokenClient: TokenClient;

public constructor(settings: OidcClientSettingsStore, metadataService: MetadataService) {
this._settings = settings;
this._metadataService = metadataService;
this._userInfoService = new UserInfoService(metadataService);
this._tokenClient = new TokenClient(this._settings, metadataService);
}

Expand Down Expand Up @@ -119,6 +122,26 @@ export class ResponseValidator {
if (response.isOpenIdConnect) {
Log.debug("ResponseValidator._processClaims: response is OIDC, processing claims");
response.profile = this._filterProtocolClaims(response.profile);

if (state.skipUserInfo !== true && this._settings.loadUserInfo && response.access_token) {
Log.debug("ResponseValidator._processClaims: loading user info");

const claims = await this._userInfoService.getClaims(response.access_token);
Log.debug("ResponseValidator._processClaims: user info claims received from user info endpoint");

if (claims.sub !== response.profile.sub) {
Log.error("ResponseValidator._processClaims: sub from user info endpoint does not match sub in id_token");
throw new Error("sub from user info endpoint does not match sub in id_token");
}

response.profile = this._mergeClaims(response.profile, claims);
Log.debug("ResponseValidator._processClaims: user info claims received, updated profile:", response.profile);

return response;
}
else {
Log.debug("ResponseValidator._processClaims: not loading user info");
}
}
else {
Log.debug("ResponseValidator._processClaims: response is not OIDC, not processing claims");
Expand All @@ -127,6 +150,39 @@ export class ResponseValidator {
return response;
}

protected _mergeClaims(claims1: UserProfile, claims2: any): UserProfile {
const result = Object.assign({}, claims1 as Record<string, any>);

for (const name in claims2) {
let values = claims2[name];
if (!Array.isArray(values)) {
values = [values];
}

for (let i = 0; i < values.length; i++) {
const value = values[i];
if (!result[name]) {
result[name] = value;
}
else if (Array.isArray(result[name])) {
if (result[name].indexOf(value) < 0) {
result[name].push(value);
}
}
else if (result[name] !== value) {
if (typeof value === "object" && this._settings.mergeClaims) {
result[name] = this._mergeClaims(result[name], value);
}
else {
result[name] = [result[name], value];
}
}
}
}

return result;
}

protected _filterProtocolClaims(claims: UserProfile): UserProfile {
Log.debug("ResponseValidator._filterProtocolClaims, incoming claims:", claims);

Expand Down Expand Up @@ -175,13 +231,33 @@ export class ResponseValidator {
response.error_description = tokenResponse.error_description || response.error_description;
response.error_uri = tokenResponse.error_uri || response.error_uri;

response.id_token = tokenResponse.id_token || response.id_token;
response.session_state = tokenResponse.session_state || response.session_state;
response.access_token = tokenResponse.access_token || response.access_token;
response.token_type = tokenResponse.token_type || response.token_type;
response.scope = tokenResponse.scope || response.scope;
response.expires_in = parseInt(tokenResponse.expires_in) || response.expires_in;

if (response.id_token) {
Log.debug("ResponseValidator._processCode: token response successful, processing id_token");
return this._validateIdTokenAttributes(state, response, response.id_token);
}

Log.debug("ResponseValidator._processCode: token response successful, returning response");
return response;
}

protected async _validateIdTokenAttributes(state: SigninState, response: SigninResponse, id_token: string): Promise<SigninResponse> {
Log.debug("ResponseValidator._validateIdTokenAttributes: Decoding JWT attributes");

const payload = JwtUtils.decode(id_token);

if (!payload.sub) {
Log.error("ResponseValidator._validateIdTokenAttributes: No sub present in id_token");
throw new Error("No sub present in id_token");
}

response.profile = payload;
return response;
}
}
5 changes: 3 additions & 2 deletions src/SigninRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export interface SigninRequestArgs {
display?: string;
max_age?: number;
ui_locales?: string;
id_token_hint?: string;
login_hint?: string;
acr_values?: string;
resource?: string;
Expand All @@ -40,7 +41,7 @@ export class SigninRequest {
// mandatory
url, authority, client_id, redirect_uri, response_type, scope,
// optional
state_data, prompt, display, max_age, ui_locales, login_hint, acr_values, resource, response_mode,
state_data, prompt, display, max_age, ui_locales, id_token_hint, login_hint, acr_values, resource, response_mode,
request, request_uri, extraQueryParams, request_type, client_secret, extraTokenParams, skipUserInfo
}: SigninRequestArgs) {
if (!url) {
Expand Down Expand Up @@ -93,7 +94,7 @@ export class SigninRequest {
url = UrlUtils.addQueryParam(url, "code_challenge_method", "S256");
}

const optional: Record<string, any> = { prompt, display, max_age, ui_locales, login_hint, acr_values, resource, request, request_uri, response_mode };
const optional: Record<string, any> = { prompt, display, max_age, ui_locales, id_token_hint, login_hint, acr_values, resource, request, request_uri, response_mode };
for (const key in optional) {
if (optional[key]) {
url = UrlUtils.addQueryParam(url, key, optional[key]);
Expand Down
Loading

0 comments on commit 310b0f2

Please sign in to comment.