mirror of
https://github.com/CommunitySolidServer/CommunitySolidServer.git
synced 2024-10-03 14:55:10 +00:00
feat: Simplify and merge OIDC configurations
This commit is contained in:
@@ -1,96 +0,0 @@
|
||||
import type { AnyObject,
|
||||
CanBePromise,
|
||||
interactionPolicy as InteractionPolicy,
|
||||
KoaContextWithOIDC,
|
||||
Configuration,
|
||||
Account,
|
||||
ErrorOut } from 'oidc-provider';
|
||||
import { Provider } from 'oidc-provider';
|
||||
import type { ErrorHandler } from '../ldp/http/ErrorHandler';
|
||||
import type { ResponseWriter } from '../ldp/http/ResponseWriter';
|
||||
import type { RepresentationPreferences } from '../ldp/representation/RepresentationPreferences';
|
||||
import type { ConfigurationFactory } from './configuration/ConfigurationFactory';
|
||||
|
||||
/**
|
||||
* Creates a Provider from the oidc-provider library.
|
||||
* This can be used for handling many of the oidc interactions during the IDP process.
|
||||
* Full documentation can be found at https://github.com/panva/node-oidc-provider/blob/v6.x/docs/README.md
|
||||
*/
|
||||
export class IdentityProviderFactory {
|
||||
private readonly issuer: string;
|
||||
private readonly configurationFactory: ConfigurationFactory;
|
||||
private readonly errorHandler: ErrorHandler;
|
||||
private readonly responseWriter: ResponseWriter;
|
||||
|
||||
public constructor(issuer: string, configurationFactory: ConfigurationFactory,
|
||||
errorHandler: ErrorHandler, responseWriter: ResponseWriter) {
|
||||
this.issuer = issuer;
|
||||
this.configurationFactory = configurationFactory;
|
||||
this.errorHandler = errorHandler;
|
||||
this.responseWriter = responseWriter;
|
||||
}
|
||||
|
||||
public async createProvider(interactionPolicyOptions: {
|
||||
policy?: InteractionPolicy.Prompt[];
|
||||
url?: (ctx: KoaContextWithOIDC) => CanBePromise<string>;
|
||||
}): Promise<Provider> {
|
||||
const configuration = await this.configurationFactory.createConfiguration();
|
||||
const augmentedConfig: Configuration = {
|
||||
...configuration,
|
||||
interactions: {
|
||||
policy: interactionPolicyOptions.policy,
|
||||
url: interactionPolicyOptions.url,
|
||||
},
|
||||
async findAccount(ctx: KoaContextWithOIDC, sub: string): Promise<Account> {
|
||||
return {
|
||||
accountId: sub,
|
||||
async claims(): Promise<{ sub: string; [key: string]: any }> {
|
||||
return {
|
||||
sub,
|
||||
webid: sub,
|
||||
};
|
||||
},
|
||||
};
|
||||
},
|
||||
claims: {
|
||||
...configuration.claims,
|
||||
webid: [ 'webid', 'client_webid' ],
|
||||
},
|
||||
conformIdTokenClaims: false,
|
||||
features: {
|
||||
...configuration.features,
|
||||
registration: { enabled: true },
|
||||
dPoP: { enabled: true, ack: 'draft-01' },
|
||||
claimsParameter: { enabled: true },
|
||||
},
|
||||
subjectTypes: [ 'public', 'pairwise' ],
|
||||
formats: {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
AccessToken: 'jwt',
|
||||
},
|
||||
audiences(): string {
|
||||
return 'solid';
|
||||
},
|
||||
extraAccessTokenClaims(ctx, token): CanBePromise<AnyObject | void | undefined> {
|
||||
if ((token as any).accountId) {
|
||||
return {
|
||||
webid: (token as any).accountId,
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
client_webid: 'http://localhost:3001/',
|
||||
aud: 'solid',
|
||||
};
|
||||
}
|
||||
return {};
|
||||
},
|
||||
renderError:
|
||||
async(ctx: KoaContextWithOIDC, out: ErrorOut, error: Error): Promise<void> => {
|
||||
// This allows us to stream directly to to the response object, see https://github.com/koajs/koa/issues/944
|
||||
ctx.respond = false;
|
||||
const preferences: RepresentationPreferences = { type: { 'text/plain': 1 }};
|
||||
const result = await this.errorHandler.handleSafe({ error, preferences });
|
||||
await this.responseWriter.handleSafe({ response: ctx.res, result });
|
||||
},
|
||||
};
|
||||
return new Provider(this.issuer, augmentedConfig);
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,16 @@
|
||||
import type { Provider } from 'oidc-provider';
|
||||
import type { ErrorHandler } from '../ldp/http/ErrorHandler';
|
||||
import type { ResponseWriter } from '../ldp/http/ResponseWriter';
|
||||
import type { RepresentationPreferences } from '../ldp/representation/RepresentationPreferences';
|
||||
import { getLoggerFor } from '../logging/LogUtil';
|
||||
import type { HttpHandlerInput } from '../server/HttpHandler';
|
||||
import { HttpHandler } from '../server/HttpHandler';
|
||||
import { assertError, createErrorMessage } from '../util/errors/ErrorUtil';
|
||||
import type { IdentityProviderFactory } from './IdentityProviderFactory';
|
||||
import { assertError } from '../util/errors/ErrorUtil';
|
||||
import type { ProviderFactory } from './configuration/ProviderFactory';
|
||||
import type { InteractionHttpHandler } from './interaction/InteractionHttpHandler';
|
||||
import type { InteractionPolicy } from './interaction/InteractionPolicy';
|
||||
|
||||
/**
|
||||
* Handles all requests relevant for the entire IDP interaction,
|
||||
* by sending them to either the stored {@link InteractionHttpHandler},
|
||||
* or the generated {@link Provider} if the first does not support the request.
|
||||
* or the generated Provider from the {@link ProviderFactory} if the first does not support the request.
|
||||
*
|
||||
* The InteractionHttpHandler would handle all requests where we need custom behaviour,
|
||||
* such as everything related to generating and validating an account.
|
||||
@@ -25,46 +22,28 @@ import type { InteractionPolicy } from './interaction/InteractionPolicy';
|
||||
export class IdentityProviderHttpHandler extends HttpHandler {
|
||||
protected readonly logger = getLoggerFor(this);
|
||||
|
||||
private readonly providerFactory: IdentityProviderFactory;
|
||||
private readonly interactionPolicy: InteractionPolicy;
|
||||
private readonly providerFactory: ProviderFactory;
|
||||
private readonly interactionHttpHandler: InteractionHttpHandler;
|
||||
private readonly errorHandler: ErrorHandler;
|
||||
private readonly responseWriter: ResponseWriter;
|
||||
private provider?: Provider;
|
||||
|
||||
public constructor(
|
||||
providerFactory: IdentityProviderFactory,
|
||||
interactionPolicy: InteractionPolicy,
|
||||
providerFactory: ProviderFactory,
|
||||
interactionHttpHandler: InteractionHttpHandler,
|
||||
errorHandler: ErrorHandler,
|
||||
responseWriter: ResponseWriter,
|
||||
) {
|
||||
super();
|
||||
this.providerFactory = providerFactory;
|
||||
this.interactionPolicy = interactionPolicy;
|
||||
this.interactionHttpHandler = interactionHttpHandler;
|
||||
this.errorHandler = errorHandler;
|
||||
this.responseWriter = responseWriter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the provider or retrieve it if it has already been created.
|
||||
*/
|
||||
private async getProvider(): Promise<Provider> {
|
||||
if (!this.provider) {
|
||||
try {
|
||||
this.provider = await this.providerFactory.createProvider(this.interactionPolicy);
|
||||
} catch (err: unknown) {
|
||||
this.logger.error(`Failed to create Provider: ${createErrorMessage(err)}`);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
return this.provider;
|
||||
}
|
||||
|
||||
public async handle(input: HttpHandlerInput): Promise<void> {
|
||||
const provider = await this.getProvider();
|
||||
const provider = await this.providerFactory.getProvider();
|
||||
|
||||
// If our own interaction handler does not support the input, it must be a request for the OIDC library
|
||||
try {
|
||||
await this.interactionHttpHandler.canHandle({ ...input, provider });
|
||||
} catch {
|
||||
@@ -76,8 +55,8 @@ export class IdentityProviderHttpHandler extends HttpHandler {
|
||||
await this.interactionHttpHandler.handle({ ...input, provider });
|
||||
} catch (error: unknown) {
|
||||
assertError(error);
|
||||
const preferences: RepresentationPreferences = { type: { 'text/plain': 1 }};
|
||||
const result = await this.errorHandler.handleSafe({ error, preferences });
|
||||
// Setting preferences to text/plain since we didn't parse accept headers, see #764
|
||||
const result = await this.errorHandler.handleSafe({ error, preferences: { type: { 'text/plain': 1 }}});
|
||||
await this.responseWriter.handleSafe({ response: input.response, result });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
import type { Configuration } from 'oidc-provider';
|
||||
|
||||
/**
|
||||
* Creates an IDP Configuration to be used in
|
||||
* panva/node-oidc-provider
|
||||
*/
|
||||
export interface ConfigurationFactory {
|
||||
createConfiguration: () => Promise<Configuration>;
|
||||
}
|
||||
244
src/identity/configuration/IdentityProviderFactory.ts
Normal file
244
src/identity/configuration/IdentityProviderFactory.ts
Normal file
@@ -0,0 +1,244 @@
|
||||
/* eslint-disable @typescript-eslint/naming-convention, import/no-unresolved, tsdoc/syntax */
|
||||
// import/no-unresolved can't handle jose imports
|
||||
// tsdoc/syntax can't handle {json} parameter
|
||||
import { randomBytes } from 'crypto';
|
||||
import type { JWK } from 'jose/jwk/from_key_like';
|
||||
import { fromKeyLike } from 'jose/jwk/from_key_like';
|
||||
import { generateKeyPair } from 'jose/util/generate_key_pair';
|
||||
import type { AnyObject,
|
||||
CanBePromise,
|
||||
KoaContextWithOIDC,
|
||||
Configuration,
|
||||
Account,
|
||||
ErrorOut, Adapter } from 'oidc-provider';
|
||||
import { Provider } from 'oidc-provider';
|
||||
import urljoin from 'url-join';
|
||||
import type { ErrorHandler } from '../../ldp/http/ErrorHandler';
|
||||
import type { ResponseWriter } from '../../ldp/http/ResponseWriter';
|
||||
import type { KeyValueStorage } from '../../storage/keyvalue/KeyValueStorage';
|
||||
import { ensureTrailingSlash } from '../../util/PathUtil';
|
||||
import type { AdapterFactory } from '../storage/AdapterFactory';
|
||||
import type { ProviderFactory } from './ProviderFactory';
|
||||
|
||||
export interface IdentityProviderFactoryArgs {
|
||||
/**
|
||||
* Factory that creates the adapter used for OIDC data storage.
|
||||
*/
|
||||
adapterFactory: AdapterFactory;
|
||||
/**
|
||||
* Base URL of the server.
|
||||
*/
|
||||
baseUrl: string;
|
||||
/**
|
||||
* Path of the IDP component in the server.
|
||||
* Should start with a slash.
|
||||
*/
|
||||
idpPath: string;
|
||||
/**
|
||||
* Storage used to store cookie and JWT keys so they can be re-used in case of multithreading.
|
||||
*/
|
||||
storage: KeyValueStorage<string, unknown>;
|
||||
/**
|
||||
* Used to convert errors thrown by the OIDC library.
|
||||
*/
|
||||
errorHandler: ErrorHandler;
|
||||
/**
|
||||
* Used to write out errors thrown by the OIDC library.
|
||||
*/
|
||||
responseWriter: ResponseWriter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an OIDC Provider based on the provided configuration and parameters.
|
||||
* The provider will be cached and returned on subsequent calls.
|
||||
* Cookie and JWT keys will be stored in an internal storage so they can be re-used over multiple threads.
|
||||
* Necessary claims for Solid OIDC interactions will be added.
|
||||
* Routes will be updated based on the `baseUrl` and `idpPath`.
|
||||
*/
|
||||
export class IdentityProviderFactory implements ProviderFactory {
|
||||
private readonly config: Configuration;
|
||||
private readonly adapterFactory!: AdapterFactory;
|
||||
private readonly baseUrl!: string;
|
||||
private readonly idpPath!: string;
|
||||
private readonly storage!: KeyValueStorage<string, unknown>;
|
||||
private readonly errorHandler!: ErrorHandler;
|
||||
private readonly responseWriter!: ResponseWriter;
|
||||
|
||||
private provider?: Provider;
|
||||
|
||||
/**
|
||||
* @param config - JSON config for the OIDC library @range {json}
|
||||
* @param args - Remaining parameters required for the factory.
|
||||
*/
|
||||
public constructor(config: Configuration, args: IdentityProviderFactoryArgs) {
|
||||
if (!args.idpPath.startsWith('/')) {
|
||||
throw new Error('idpPath needs to start with a /');
|
||||
}
|
||||
this.config = config;
|
||||
Object.assign(this, args);
|
||||
}
|
||||
|
||||
public async getProvider(): Promise<Provider> {
|
||||
if (this.provider) {
|
||||
return this.provider;
|
||||
}
|
||||
this.provider = await this.createProvider();
|
||||
return this.provider;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a Provider by building a Configuration using all the stored parameters.
|
||||
*/
|
||||
private async createProvider(): Promise<Provider> {
|
||||
const config = await this.initConfig();
|
||||
|
||||
// Add correct claims to IdToken/AccessToken responses
|
||||
this.configureClaims(config);
|
||||
|
||||
// Make sure routes are contained in the IDP space
|
||||
this.configureRoutes(config);
|
||||
|
||||
// Render errors with our own error handler
|
||||
this.configureErrors(config);
|
||||
|
||||
return new Provider(this.baseUrl, config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a configuration by copying the internal configuration
|
||||
* and adding the adapter, default audience and jwks/cookie keys.
|
||||
*/
|
||||
private async initConfig(): Promise<Configuration> {
|
||||
// Create a deep copy
|
||||
const config: Configuration = JSON.parse(JSON.stringify(this.config));
|
||||
|
||||
// Indicates which Adapter should be used for storing oidc data
|
||||
// The adapter function MUST be a named function.
|
||||
// See https://github.com/panva/node-oidc-provider/issues/799
|
||||
const factory = this.adapterFactory;
|
||||
config.adapter = function loadAdapter(name: string): Adapter {
|
||||
return factory.createStorageAdapter(name);
|
||||
};
|
||||
|
||||
// This needs to be a function, can't have a static string
|
||||
// Sets the "aud" value to "solid" for all tokens
|
||||
config.audiences = (): string => 'solid';
|
||||
|
||||
// Cast necessary due to typing conflict between jose 2.x and 3.x
|
||||
config.jwks = await this.generateJwks() as any;
|
||||
config.cookies = {
|
||||
...config.cookies ?? {},
|
||||
keys: await this.generateCookieKeys(),
|
||||
};
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a JWKS using a single RS256 JWK..
|
||||
* The JWKS will be cached so subsequent calls return the same key.
|
||||
*/
|
||||
private async generateJwks(): Promise<{ keys: JWK[] }> {
|
||||
// Check to see if the keys are already saved
|
||||
const key = `${this.idpPath}/jwks`;
|
||||
const jwks = await this.storage.get(key) as { keys: JWK[] } | undefined;
|
||||
if (jwks) {
|
||||
return jwks;
|
||||
}
|
||||
// If they are not, generate and save them
|
||||
const { privateKey } = await generateKeyPair('RS256');
|
||||
const jwk = await fromKeyLike(privateKey);
|
||||
// Required for Solid authn client
|
||||
jwk.alg = 'RS256';
|
||||
// In node v15.12.0 the JWKS does not get accepted because the JWK is not a plain object,
|
||||
// which is why we convert it into a plain object here.
|
||||
// Potentially this can be changed at a later point in time to `{ keys: [ jwk ]}`.
|
||||
const newJwks = { keys: [{ ...jwk }]};
|
||||
await this.storage.set(key, newJwks);
|
||||
return newJwks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a cookie secret to be used for cookie signing.
|
||||
* The key will be cached so subsequent calls return the same key.
|
||||
*/
|
||||
private async generateCookieKeys(): Promise<string[]> {
|
||||
// Check to see if the keys are already saved
|
||||
const key = `${this.idpPath}/cookie-secret`;
|
||||
const cookieSecret = await this.storage.get(key);
|
||||
if (Array.isArray(cookieSecret)) {
|
||||
return cookieSecret;
|
||||
}
|
||||
// If they are not, generate and save them
|
||||
const newCookieSecret = [ randomBytes(64).toString('hex') ];
|
||||
await this.storage.set(key, newCookieSecret);
|
||||
return newCookieSecret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the necessary claims the to id token and access token based on the Solid OIDC spec.
|
||||
*/
|
||||
private configureClaims(config: Configuration): void {
|
||||
// Returns the id_token and adds the webid claim
|
||||
config.findAccount = async(ctx: KoaContextWithOIDC, sub: string): Promise<Account> => ({
|
||||
accountId: sub,
|
||||
claims: async(): Promise<{ sub: string; [key: string]: any }> => ({ sub, webid: sub }),
|
||||
});
|
||||
|
||||
// Add extra claims in case an AccessToken is being issued.
|
||||
// Specifically this sets the required webid and client_id claims for the access token
|
||||
config.extraAccessTokenClaims = (ctx, token): CanBePromise<AnyObject | void> =>
|
||||
// AccessToken interface is not exported
|
||||
(token as any).accountId ?
|
||||
{ webid: (token as any).accountId, client_id: 'http://localhost:3001/' } :
|
||||
{};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the route string as required by the `oidc-provider` library.
|
||||
* In case base URL is `http://test.com/foo/`, `idpPath` is `/idp` and `relative` is `device/auth`,
|
||||
* this would result in `/foo/idp/device/auth`.
|
||||
*/
|
||||
private createRoute(relative: string): string {
|
||||
return new URL(urljoin(this.baseUrl, this.idpPath, relative)).pathname;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up all the IDP routes relative to the IDP path.
|
||||
*/
|
||||
private configureRoutes(config: Configuration): void {
|
||||
// When oidc-provider cannot fulfill the authorization request for any of the possible reasons
|
||||
// (missing user session, requested ACR not fulfilled, prompt requested, ...)
|
||||
// it will resolve the interactions.url helper function and redirect the User-Agent to that url.
|
||||
config.interactions = {
|
||||
url: (): string => ensureTrailingSlash(this.idpPath),
|
||||
};
|
||||
|
||||
config.routes = {
|
||||
authorization: this.createRoute('auth'),
|
||||
check_session: this.createRoute('session/check'),
|
||||
code_verification: this.createRoute('device'),
|
||||
device_authorization: this.createRoute('device/auth'),
|
||||
end_session: this.createRoute('session/end'),
|
||||
introspection: this.createRoute('token/introspection'),
|
||||
jwks: this.createRoute('jwks'),
|
||||
pushed_authorization_request: this.createRoute('request'),
|
||||
registration: this.createRoute('reg'),
|
||||
revocation: this.createRoute('token/revocation'),
|
||||
token: this.createRoute('token'),
|
||||
userinfo: this.createRoute('me'),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Pipes library errors to the provided ErrorHandler and ResponseWriter.
|
||||
*/
|
||||
private configureErrors(config: Configuration): void {
|
||||
config.renderError = async(ctx: KoaContextWithOIDC, out: ErrorOut, error: Error): Promise<void> => {
|
||||
// This allows us to stream directly to to the response object, see https://github.com/koajs/koa/issues/944
|
||||
ctx.respond = false;
|
||||
const result = await this.errorHandler.handleSafe({ error, preferences: { type: { 'text/plain': 1 }}});
|
||||
await this.responseWriter.handleSafe({ response: ctx.res, result });
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,138 +0,0 @@
|
||||
/* eslint-disable @typescript-eslint/naming-convention, import/no-unresolved */
|
||||
// import/no-unresolved can't handle jose imports
|
||||
import { randomBytes } from 'crypto';
|
||||
import type { JWK } from 'jose/jwk/from_key_like';
|
||||
import { fromKeyLike } from 'jose/jwk/from_key_like';
|
||||
import { generateKeyPair } from 'jose/util/generate_key_pair';
|
||||
import type { Adapter, Configuration } from 'oidc-provider';
|
||||
import urljoin from 'url-join';
|
||||
import type { KeyValueStorage } from '../../storage/keyvalue/KeyValueStorage';
|
||||
import { ensureTrailingSlash, trimTrailingSlashes } from '../../util/PathUtil';
|
||||
import type { AdapterFactory } from '../storage/AdapterFactory';
|
||||
import type { ConfigurationFactory } from './ConfigurationFactory';
|
||||
|
||||
/**
|
||||
* An IDP Configuration Factory that generates and saves keys
|
||||
* to the provided key value store.
|
||||
*/
|
||||
export class KeyConfigurationFactory implements ConfigurationFactory {
|
||||
private readonly adapterFactory: AdapterFactory;
|
||||
private readonly baseUrl: string;
|
||||
private readonly idpPath: string;
|
||||
private readonly storage: KeyValueStorage<string, unknown>;
|
||||
|
||||
public constructor(
|
||||
adapterFactory: AdapterFactory,
|
||||
baseUrl: string,
|
||||
idpPath: string,
|
||||
storage: KeyValueStorage<string, unknown>,
|
||||
) {
|
||||
this.adapterFactory = adapterFactory;
|
||||
this.baseUrl = ensureTrailingSlash(baseUrl);
|
||||
this.idpPath = trimTrailingSlashes(idpPath);
|
||||
this.storage = storage;
|
||||
}
|
||||
|
||||
private get jwksKey(): string {
|
||||
return `${this.idpPath}/jwks`;
|
||||
}
|
||||
|
||||
private async generateJwks(): Promise<{ keys: JWK[] }> {
|
||||
// Check to see if the keys are already saved
|
||||
const jwks = await this.storage.get(this.jwksKey) as { keys: JWK[] } | undefined;
|
||||
if (jwks) {
|
||||
return jwks;
|
||||
}
|
||||
// If they are not, generate and save them
|
||||
const { privateKey } = await generateKeyPair('RS256');
|
||||
const jwk = await fromKeyLike(privateKey);
|
||||
// Required for Solid authn client
|
||||
jwk.alg = 'RS256';
|
||||
// In node v15.12.0 the JWKS does not get accepted because the JWK is not a plain object,
|
||||
// which is why we convert it into a plain object here.
|
||||
// Potentially this can be changed at a later point in time to `{ keys: [ jwk ]}`.
|
||||
const newJwks = { keys: [{ ...jwk }]};
|
||||
await this.storage.set(this.jwksKey, newJwks);
|
||||
return newJwks;
|
||||
}
|
||||
|
||||
private get cookieSecretKey(): string {
|
||||
return `${this.idpPath}/cookie-secret`;
|
||||
}
|
||||
|
||||
private async generateCookieKeys(): Promise<string[]> {
|
||||
// Check to see if the keys are already saved
|
||||
const cookieSecret = await this.storage.get(this.cookieSecretKey);
|
||||
if (Array.isArray(cookieSecret)) {
|
||||
return cookieSecret;
|
||||
}
|
||||
// If they are not, generate and save them
|
||||
const newCookieSecret = [ randomBytes(64).toString('hex') ];
|
||||
await this.storage.set(this.cookieSecretKey, newCookieSecret);
|
||||
return newCookieSecret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the route string as required by the `oidc-provider` library.
|
||||
* In case base URL is `http://test.com/foo/`, `idpPath` is `/idp` and `relative` is `device/auth`,
|
||||
* this would result in `/foo/idp/device/auth`.
|
||||
*/
|
||||
private createRoute(relative: string): string {
|
||||
return new URL(urljoin(this.baseUrl, this.idpPath, relative)).pathname;
|
||||
}
|
||||
|
||||
public async createConfiguration(): Promise<Configuration> {
|
||||
// Cast necessary due to typing conflict between jose 2.x and 3.x
|
||||
const jwks = await this.generateJwks() as any;
|
||||
const cookieKeys = await this.generateCookieKeys();
|
||||
|
||||
// The adapter function MUST be a named function.
|
||||
// See https://github.com/panva/node-oidc-provider/issues/799
|
||||
const factory = this.adapterFactory;
|
||||
return {
|
||||
adapter: function loadAdapter(name: string): Adapter {
|
||||
return factory.createStorageAdapter(name);
|
||||
},
|
||||
cookies: {
|
||||
long: { signed: true, maxAge: 1 * 24 * 60 * 60 * 1000 },
|
||||
short: { signed: true },
|
||||
keys: cookieKeys,
|
||||
},
|
||||
conformIdTokenClaims: false,
|
||||
features: {
|
||||
devInteractions: { enabled: false },
|
||||
deviceFlow: { enabled: true },
|
||||
introspection: { enabled: true },
|
||||
revocation: { enabled: true },
|
||||
registration: { enabled: true },
|
||||
claimsParameter: { enabled: true },
|
||||
},
|
||||
jwks,
|
||||
ttl: {
|
||||
AccessToken: 1 * 60 * 60,
|
||||
AuthorizationCode: 10 * 60,
|
||||
IdToken: 1 * 60 * 60,
|
||||
DeviceCode: 10 * 60,
|
||||
RefreshToken: 1 * 24 * 60 * 60,
|
||||
},
|
||||
subjectTypes: [ 'public', 'pairwise' ],
|
||||
routes: {
|
||||
authorization: this.createRoute('auth'),
|
||||
check_session: this.createRoute('session/check'),
|
||||
code_verification: this.createRoute('device'),
|
||||
device_authorization: this.createRoute('device/auth'),
|
||||
end_session: this.createRoute('session/end'),
|
||||
introspection: this.createRoute('token/introspection'),
|
||||
jwks: this.createRoute('jwks'),
|
||||
pushed_authorization_request: this.createRoute('request'),
|
||||
registration: this.createRoute('reg'),
|
||||
revocation: this.createRoute('token/revocation'),
|
||||
token: this.createRoute('token'),
|
||||
userinfo: this.createRoute('me'),
|
||||
},
|
||||
discovery: {
|
||||
solid_oidc_supported: 'https://solidproject.org/TR/solid-oidc',
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
13
src/identity/configuration/ProviderFactory.ts
Normal file
13
src/identity/configuration/ProviderFactory.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import type { Provider } from 'oidc-provider';
|
||||
|
||||
/**
|
||||
* Returns a Provider of OIDC interactions.
|
||||
*/
|
||||
export interface ProviderFactory {
|
||||
/**
|
||||
* Gets a provider from the factory.
|
||||
* Multiple calls to this function should return providers that produce the same results.
|
||||
* This is mostly relevant for signing keys.
|
||||
*/
|
||||
getProvider: () => Promise<Provider>;
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
import type { CanBePromise, interactionPolicy, KoaContextWithOIDC } from 'oidc-provider';
|
||||
|
||||
/**
|
||||
* Config options to communicate exactly how to handle requests.
|
||||
*/
|
||||
export interface InteractionPolicy {
|
||||
policy: interactionPolicy.Prompt[];
|
||||
url: (ctx: KoaContextWithOIDC) => CanBePromise<string>;
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
import type { KoaContextWithOIDC } from 'oidc-provider';
|
||||
import { interactionPolicy } from 'oidc-provider';
|
||||
import { ensureTrailingSlash } from '../../../util/PathUtil';
|
||||
import type {
|
||||
InteractionPolicy,
|
||||
} from '../InteractionPolicy';
|
||||
|
||||
/**
|
||||
* Interaction policy that redirects to `idpPath`.
|
||||
* Uses the `select_account` interaction policy.
|
||||
*/
|
||||
export class AccountInteractionPolicy implements InteractionPolicy {
|
||||
public readonly policy: interactionPolicy.Prompt[];
|
||||
public readonly url: (ctx: KoaContextWithOIDC) => string;
|
||||
|
||||
public constructor(idpPath: string) {
|
||||
if (!idpPath.startsWith('/')) {
|
||||
throw new Error('idpPath needs to start with a /');
|
||||
}
|
||||
const interactions = interactionPolicy.base();
|
||||
const selectAccount = new interactionPolicy.Prompt({
|
||||
name: 'select_account',
|
||||
requestable: true,
|
||||
});
|
||||
interactions.add(selectAccount, 0);
|
||||
this.policy = interactions;
|
||||
|
||||
// When oidc-provider cannot fulfill the authorization request for any of the possible reasons
|
||||
// (missing user session, requested ACR not fulfilled, prompt requested, ...)
|
||||
// it will resolve the interactions.url helper function and redirect the User-Agent to that url.
|
||||
this.url = (): string => ensureTrailingSlash(idpPath);
|
||||
}
|
||||
}
|
||||
@@ -16,8 +16,8 @@ export * from './authorization/WebAclAuthorization';
|
||||
export * from './authorization/WebAclAuthorizer';
|
||||
|
||||
// Identity/Configuration
|
||||
export * from './identity/configuration/ConfigurationFactory';
|
||||
export * from './identity/configuration/KeyConfigurationFactory';
|
||||
export * from './identity/configuration/IdentityProviderFactory';
|
||||
export * from './identity/configuration/ProviderFactory';
|
||||
|
||||
// Identity/Interaction/Email-Password/Handler
|
||||
export * from './identity/interaction/email-password/handler/ForgotPasswordHandler';
|
||||
@@ -32,7 +32,6 @@ export * from './identity/interaction/email-password/storage/AccountStore';
|
||||
export * from './identity/interaction/email-password/storage/BaseAccountStore';
|
||||
|
||||
// Identity/Interaction/Email-Password
|
||||
export * from './identity/interaction/email-password/AccountInteractionPolicy';
|
||||
export * from './identity/interaction/email-password/EmailPasswordUtil';
|
||||
|
||||
// Identity/Interaction/Util
|
||||
@@ -49,7 +48,6 @@ export * from './identity/interaction/util/TemplateRenderer';
|
||||
|
||||
// Identity/Interaction
|
||||
export * from './identity/interaction/InteractionHttpHandler';
|
||||
export * from './identity/interaction/InteractionPolicy';
|
||||
export * from './identity/interaction/SessionHttpHandler';
|
||||
|
||||
// Identity/Ownership
|
||||
@@ -63,7 +61,6 @@ export * from './identity/storage/ExpiringAdapterFactory';
|
||||
export * from './identity/storage/WrappedFetchAdapterFactory';
|
||||
|
||||
// Identity
|
||||
export * from './identity/IdentityProviderFactory';
|
||||
export * from './identity/IdentityProviderHttpHandler';
|
||||
|
||||
// Init/Final
|
||||
|
||||
Reference in New Issue
Block a user