Skip to content

Commit

Permalink
Merge pull request #341 from vscheuber/main
Browse files Browse the repository at this point in the history
fix journey export bug and add token cache and auto token refresh
  • Loading branch information
vscheuber authored Nov 2, 2023
2 parents 19dd8e9 + 0bc1415 commit 8651515
Show file tree
Hide file tree
Showing 34 changed files with 5,261 additions and 2,802 deletions.
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- \#53: Frodo Library now uses a file-based secure token cache to persist session and access tokens for re-use. The cached tokens are protected by the credential that was used to obtain them. Session tokens are encrypted using the hashed password as the master key, access tokens are encrypted using the hashed JWK private key as the master key. Therefore only users and processes with the correct credentials can access the tokens in the cache.

- There is a new TokenCache module with accessible functions for frodo clients (like frodo-cli) to use.
- The State module has been extended to host meta data like expiration time for sessions and tokens and a new boolean field indicating if the library should make use of the new token cache or not: `state.getUseTokenCache(): boolean` and `state.setUseTokenCache(useTokenCache: boolean): void`.
- The new default behavior is to always use the new token cache.

- \#340: Frodo Library now autotomatically refreshes expired session and access tokens.

- The new default behavior is to automatically refresh tokens. However, if an application prefers to handle that on its own, it can call the `frodo.login.getTokens()` functino with a new `autoRefresh: boolean` parameter.

### Fixed

- rockcarver/frodo-cli#316: Frodo Library now properly exports scripts referenced by the `Device Match` node if the `Use Custom Matching Script` option is selected.

## [2.0.0-46] - 2023-10-25

## [2.0.0-45] - 2023-10-23
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

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

6 changes: 3 additions & 3 deletions src/api/ApiTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@ export interface IdObjectSkeletonInterface extends NoIdObjectSkeletonInterface {
_id?: string;
}

export type ReadableStrings = string[];
export type Readable<Type> = Type;

export type WritableStrings = {
export type Writable<Type> = {
inherited: boolean;
value: string[];
value: Type;
};

export type QueryResult<Type> = {
Expand Down
1 change: 1 addition & 0 deletions src/api/NodeApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ export type NodeSkeleton = IdObjectSkeletonInterface & {
script?: string;
emailTemplateName?: string;
filteredProviders?: string[];
useScript?: boolean;
};

export type NodeTypeSkeleton = IdObjectSkeletonInterface & {
Expand Down
24 changes: 12 additions & 12 deletions src/api/OAuth2ClientApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import {
type IdObjectSkeletonInterface,
type NoIdObjectSkeletonInterface,
type PagedResult,
type ReadableStrings,
type WritableStrings,
type Readable,
type Writable,
} from './ApiTypes';
import { generateAmApi } from './BaseApi';
import { AmServiceType } from './ServiceApi';
Expand All @@ -32,26 +32,26 @@ export type OAuth2ClientSkeleton = IdObjectSkeletonInterface & {
inherited: boolean;
value: string[];
};
grantTypes?: ReadableStrings | WritableStrings;
grantTypes?: Readable<string[]> | Writable<string[]>;
isConsentImplied?: Readable<boolean> | Writable<boolean>;
tokenEndpointAuthMethod?: Readable<string> | Writable<string>;
responseTypes?: Readable<string[]> | Writable<string[]>;
[k: string]: string | number | boolean | string[] | object | undefined;
};
signEncOAuth2ClientConfig?: {
jwkSet?: Readable<string> | Writable<string>;
publicKeyLocation?: Readable<string> | Writable<string>;
[k: string]: string | number | boolean | string[] | object | undefined;
};
coreOpenIDClientConfig?: {
[k: string]: string | number | boolean | string[] | object | undefined;
};
coreOAuth2ClientConfig?: {
userpassword?: string;
clientName?: {
inherited: boolean;
value: string[];
};
accessTokenLifetime?: {
inherited: boolean;
value: number;
};
scopes?: ReadableStrings | WritableStrings;
clientName?: Readable<string[]> | Writable<string[]>;
clientType?: Readable<string> | Writable<string>;
accessTokenLifetime?: Readable<number> | Writable<number>;
scopes?: Readable<string[]> | Writable<string[]>;
defaultScopes?: {
value: string[];
[k: string]: string | number | boolean | string[] | object | undefined;
Expand Down
12 changes: 8 additions & 4 deletions src/api/OAuth2OIDCApi.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,12 @@ const mock = new MockAdapter(axios);
state.setHost('https://openam-frodo-dev.forgeblocks.com/am');
state.setRealm('alpha');
state.setCookieName('cookieName');
state.setCookieValue('cookieValue');
state.setUserSessionTokenMeta({
tokenId: 'cookieValue',
realm: '/realm',
successUrl: 'url',
expires: 0,
});
state.setDeploymentType(Constants.CLOUD_DEPLOYMENT_TYPE_KEY);

describe('OAuth2OIDCApi', () => {
Expand Down Expand Up @@ -75,12 +80,11 @@ describe('OAuth2OIDCApi', () => {
};
const response = await OAuth2OIDCApi.accessToken({
amBaseUrl: state.getHost() as string,
data: bodyFormData,
postData: bodyFormData,
config,
state,
});
expect(response.status).toBe(200);
expect(response.data.access_token).toBeTruthy();
expect(response.access_token).toBeTruthy();
});
});
});
52 changes: 42 additions & 10 deletions src/api/OAuth2OIDCApi.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { AxiosRequestConfig } from 'axios';
import { AxiosRequestConfig, AxiosResponse } from 'axios';
import qs from 'qs';
import util from 'util';

Expand All @@ -15,6 +15,37 @@ const getApiConfig = () => ({
apiVersion,
});

export type AccessTokenResponseType = {
access_token: string;
scope: string;
token_type: string;
expires_in: number;
};

export type TokenInfoResponseType = {
sub: string;
cts: string;
auditTrackingId: string;
subname: string;
iss: string;
tokenName: string;
token_type: string;
authGrantId: string;
access_token: string;
aud: string;
nbf: number;
grant_type: string;
scope: string[];
auth_time: number;
sessionToken?: string;
realm: string;
exp: number;
iat: number;
expires_in: number;
jti: string;
[k: string]: string | number | string[];
};

/**
* Perform the authorization step of the authorization code grant flow
* @param {string} amBaseUrl access management base URL
Expand All @@ -33,7 +64,7 @@ export async function authorize({
data: string;
config: AxiosRequestConfig;
state: State;
}) {
}): Promise<AxiosResponse<any, any>> {
const authorizeURL = util.format(authorizeUrlTemplate, amBaseUrl, '');
return generateOauth2Api({
resource: getApiConfig(),
Expand All @@ -48,25 +79,26 @@ export async function authorize({
* @param {string} data body form data
* @param {AxiosRequestConfig} config config axios request config object
* @param {State} state library state
* @returns {Promise} a promise resolving to an object containing the authorization server response object containing the access token
* @returns {Promise<AccessTokenResponseType>} a promise resolving to an object containing the authorization server response object containing the access token
*/
export async function accessToken({
amBaseUrl,
data,
postData,
config,
state,
}: {
amBaseUrl: string;
data: any;
postData: any;
config: AxiosRequestConfig;
state: State;
}) {
}): Promise<AccessTokenResponseType> {
const accessTokenURL = util.format(accessTokenUrlTemplate, amBaseUrl, '');
return generateOauth2Api({
const { data } = await generateOauth2Api({
resource: getApiConfig(),
requestOverride: {},
state,
}).post(accessTokenURL, data, config);
}).post(accessTokenURL, postData, config);
return data;
}

/**
Expand All @@ -84,7 +116,7 @@ export async function getTokenInfo({
amBaseUrl: string;
config: AxiosRequestConfig;
state: State;
}) {
}): Promise<TokenInfoResponseType> {
const accessTokenURL = util.format(tokenInfoUrlTemplate, amBaseUrl, '');
const { data } = await generateOauth2Api({
resource: getApiConfig(),
Expand Down Expand Up @@ -115,7 +147,7 @@ export async function clientCredentialsGrant({
clientSecret: string;
scope: string;
state: State;
}) {
}): Promise<AccessTokenResponseType> {
const urlString = util.format(
accessTokenUrlTemplate,
amBaseUrl,
Expand Down
59 changes: 59 additions & 0 deletions src/api/SessionApi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import util from 'util';

import { State } from '../shared/State';
import { getCurrentRealmPath } from '../utils/ForgeRockUtils';
import { generateAmApi } from './BaseApi';

const getSessionInfoURLTemplate = '%s/json%s/sessions/?_action=getSessionInfo';
const apiVersion = 'resource=4.0';

function getApiConfig() {
return {
apiVersion,
};
}

export type SessionInfoType = {
username: string;
universalId: string;
realm: string;
latestAccessTime: string;
maxIdleExpirationTime: string;
maxSessionExpirationTime: string;
properties: {
AMCtxId: string;
[k: string]: string;
};
};

/**
* Get session info
* @param {string} tokenId session token
* @returns {Promise<SessionInfoType>} a promise resolving to a session info object
*/
export async function getSessionInfo({
tokenId,
state,
}: {
tokenId: string;
state: State;
}): Promise<SessionInfoType> {
const urlString = util.format(
getSessionInfoURLTemplate,
state.getHost(),
getCurrentRealmPath(state)
);
const { data } = await generateAmApi({
resource: getApiConfig(),
state,
}).post(
urlString,
{
tokenId,
},
{
withCredentials: true,
}
);
return data as SessionInfoType;
}
7 changes: 6 additions & 1 deletion src/api/cloud/StartupApi.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,12 @@ const mock = new MockAdapter(axios);
state.setHost('https://openam-frodo-dev.forgeblocks.com/am');
state.setRealm('alpha');
state.setCookieName('cookieName');
state.setCookieValue('cookieValue');
state.setUserSessionTokenMeta({
tokenId: 'cookieValue',
realm: '/realm',
successUrl: 'url',
expires: 0,
});

describe('StartupApi - getStatus()', () => {
test('getStatus() 1: Get restart status - expect "ready"', async () => {
Expand Down
3 changes: 3 additions & 0 deletions src/lib/FrodoLib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import Saml2Ops, { Saml2 } from '../ops/Saml2Ops';
import ScriptOps, { Script } from '../ops/ScriptOps';
import ServiceOps, { Service } from '../ops/ServiceOps';
import ThemeOps, { Theme } from '../ops/ThemeOps';
import TokenCacheOps, { TokenCache } from '../ops/TokenCacheOps';
import VersionUtils, { Version } from '../ops/VersionUtils';
// non-instantiable modules
import ConstantsImpl, { Constants } from '../shared/Constants';
Expand Down Expand Up @@ -85,6 +86,7 @@ export type Frodo = {
};

conn: ConnectionProfile;
cache: TokenCache;

email: {
template: EmailTemplate;
Expand Down Expand Up @@ -221,6 +223,7 @@ const FrodoLib = (config: StateInterface = {}): Frodo => {
},

conn: ConnectionProfileOps(state),
cache: TokenCacheOps(state),

email: {
template: EmailTemplateOps(state),
Expand Down
Loading

0 comments on commit 8651515

Please sign in to comment.