Skip to content
Draft
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
6 changes: 6 additions & 0 deletions config/identity/handler/base/provider-factory.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@
"showStackTrace": { "@id": "urn:solid-server:default:variable:showStackTrace" },
"errorHandler": { "@id": "urn:solid-server:default:ErrorHandler" },
"responseWriter": { "@id": "urn:solid-server:default:ResponseWriter" },
"jwtAssertionsGrantHandler": {
"@id": "urn:solid-server:default:JwtAssertionsGrantHandler",
"@type": "JwtAssertionsGrantHandler",
"jwkGenerator": { "@id": "urn:solid-server:default:JwkGenerator" },
"jwtAssertionsStore": { "@id": "urn:solid-server:default:JwtAssertionsStore" }
},
"interactionRoute": { "@id": "urn:solid-server:default:IndexRoute" },
"config": {
"claims": {
Expand Down
1 change: 1 addition & 0 deletions config/identity/handler/default.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

"css:config/identity/handler/enable/account.json",
"css:config/identity/handler/enable/client-credentials.json",
"css:config/identity/handler/enable/jwt-assertions.json",
"css:config/identity/handler/enable/password.json",
"css:config/identity/handler/enable/pod.json",
"css:config/identity/handler/enable/webid.json"
Expand Down
43 changes: 43 additions & 0 deletions config/identity/handler/enable/jwt-assertions.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
{
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^7.0.0/components/context.jsonld",
"@graph": [
{
"comment": "Enable JWT assertions creation."
},
{
"@id": "urn:solid-server:default:InteractionRouteHandler",
"@type": "WaterfallHandler",
"handlers": [{ "@id": "urn:solid-server:default:AccountJwtAssertionsRouter" }]
},

{
"@id": "urn:solid-server:default:AccountControlHandler",
"@type": "ControlHandler",
"controls": [{
"ControlHandler:_controls_key": "jwtAssertions",
"ControlHandler:_controls_value": { "@id": "urn:solid-server:default:AccountJwtAssertionsRoute" }
}]
},

{
"@id": "urn:solid-server:default:HtmlViewHandler",
"@type": "HtmlViewHandler",
"templates": [{
"@id": "urn:solid-server:default:CreateJwtAssertionsHtml",
"@type": "HtmlViewEntry",
"filePath": "@css:templates/identity/account/create-jwt-assertions.html.ejs",
"route": { "@id": "urn:solid-server:default:AccountJwtAssertionsRoute" }
}]
},
{
"ControlHandler:_controls_value": {
"@id": "urn:solid-server:default:AccountHtmlControlHandler",
"@type": "ControlHandler",
"controls": [{
"ControlHandler:_controls_key": "createJwtAssertions",
"ControlHandler:_controls_value": { "@id": "urn:solid-server:default:AccountJwtAssertionsRoute" }
}]
}
}
]
}
2 changes: 2 additions & 0 deletions config/identity/handler/routing/default.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
"css:config/identity/handler/routing/account/main.json",
"css:config/identity/handler/routing/client-credentials/create.json",
"css:config/identity/handler/routing/client-credentials/resource.json",
"css:config/identity/handler/routing/jwt-assertions/create.json",
"css:config/identity/handler/routing/jwt-assertions/resource.json",
"css:config/identity/handler/routing/core/main.json",
"css:config/identity/handler/routing/oidc/main.json",
"css:config/identity/handler/routing/password/main.json",
Expand Down
26 changes: 26 additions & 0 deletions config/identity/handler/routing/jwt-assertions/create.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^7.0.0/components/context.jsonld",
"@graph": [
{
"comment": "Handles JWT assertions. These can be used to automate clients. See documentation for more info.",
"@id": "urn:solid-server:default:AccountJwtAssertionsRouter",
"@type": "AuthorizedRouteHandler",
"route": {
"@id": "urn:solid-server:default:AccountJwtAssertionsRoute",
"@type": "RelativePathInteractionRoute",
"base": { "@id": "urn:solid-server:default:AccountIdRoute" },
"relativePath": "jwt-assertions/"
},
"source": {
"@type": "ViewInteractionHandler",
"source": {
"@id": "urn:solid-server:default:CreateJwtAssertionsHandler",
"@type": "CreateJwtAssertionsHandler",
"webIdStore": { "@id": "urn:solid-server:default:WebIdStore" },
"jwtAssertionsStore": { "@id": "urn:solid-server:default:JwtAssertionsStore" },
"jwtAssertionsRoute": { "@id": "urn:solid-server:default:AccountJwtAssertionsIdRoute" }
}
}
}
]
}
45 changes: 45 additions & 0 deletions config/identity/handler/routing/jwt-assertions/resource.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
{
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^7.0.0/components/context.jsonld",
"@graph": [
{
"comment": "Handles the JWT assertions link details such as deletion.",
"@id": "urn:solid-server:default:AccountJwtAssertionsIdRouter",
"@type": "AuthorizedRouteHandler",
"route": {
"@id": "urn:solid-server:default:AccountJwtAssertionsIdRoute",
"@type": "BaseJwtAssertionsIdRoute",
"base": { "@id": "urn:solid-server:default:AccountJwtAssertionsRoute" }
},
"source": {
"@id": "urn:solid-server:default:JwtAssertionsResourceHandler",
"@type": "WaterfallHandler",
"handlers": [
{
"@type": "MethodFilterHandler",
"methods": [ "GET" ],
"source": {
"@type": "JwtAssertionsDetailsHandler",
"jwtAssertionsRoute": { "@id": "urn:solid-server:default:AccountJwtAssertionsIdRoute" },
"jwtAssertionsStore": { "@id": "urn:solid-server:default:JwtAssertionsStore" }
}
},
{
"@type": "MethodFilterHandler",
"methods": [ "DELETE" ],
"source": {
"@type": "DeleteJwtAssertionsHandler",
"jwtAssertionsRoute": { "@id": "urn:solid-server:default:AccountJwtAssertionsIdRoute" },
"jwtAssertionsStore": { "@id": "urn:solid-server:default:JwtAssertionsStore" }
}
}
]
}
},

{
"@id": "urn:solid-server:default:InteractionRouteHandler",
"@type": "WaterfallHandler",
"handlers": [{ "@id": "urn:solid-server:default:AccountJwtAssertionsIdRouter" }]
}
]
}
9 changes: 9 additions & 0 deletions config/identity/handler/storage/default.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,13 +59,21 @@
"storage": { "@id": "urn:solid-server:default:AccountStorage" }
},

{
"@id": "urn:solid-server:default:JwtAssertionsStore",
"@type": "BaseJwtAssertionsStore",
"storage": { "@id": "urn:solid-server:default:AccountStorage" },
"jwkGenerator": { "@id": "urn:solid-server:default:JwkGenerator" }
},

{
"comment": "Initialize all the stores. Also necessary on primary thread for pod seeding.",
"@id": "urn:solid-server:default:PrimaryParallelInitializer",
"@type": "ParallelHandler",
"handlers": [
{ "@id": "urn:solid-server:default:AccountStore" },
{ "@id": "urn:solid-server:default:ClientCredentialsStore" },
{ "@id": "urn:solid-server:default:JwtAssertionsStore" },
{ "@id": "urn:solid-server:default:PodStore" },
{ "@id": "urn:solid-server:default:WebIdStore" }
]
Expand All @@ -77,6 +85,7 @@
"handlers": [
{ "@id": "urn:solid-server:default:AccountStore" },
{ "@id": "urn:solid-server:default:ClientCredentialsStore" },
{ "@id": "urn:solid-server:default:JwtAssertionsStore" },
{ "@id": "urn:solid-server:default:PodStore" },
{ "@id": "urn:solid-server:default:WebIdStore" }
]
Expand Down
16 changes: 16 additions & 0 deletions src/identity/IdentityUtil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,19 @@
}
return import('oidc-provider');
}

export function importDpopValidate(): CanBePromise<any> {

Check failure on line 20 in src/identity/IdentityUtil.ts

View workflow job for this annotation

GitHub Actions / npm-test / lint

Unexpected any. Specify a different type
if (process.env.JEST_WORKER_ID ?? process.env.NODE_ENV === 'test') {
return jest.requireActual('oidc-provider/lib/helpers/validate_dpop.js');
}
// @ts-expect-error

Check failure on line 24 in src/identity/IdentityUtil.ts

View workflow job for this annotation

GitHub Actions / npm-test / lint

Do not use "@ts-expect-error" because it alters compilation errors
return import('oidc-provider/lib/helpers/validate_dpop.js');

Check failure on line 25 in src/identity/IdentityUtil.ts

View workflow job for this annotation

GitHub Actions / npm-test / lint

Unexpected use of file extension "js" for "oidc-provider/lib/helpers/validate_dpop.js"
}

export function importCheckResource(): CanBePromise<any> {

Check failure on line 28 in src/identity/IdentityUtil.ts

View workflow job for this annotation

GitHub Actions / npm-test / lint

Unexpected any. Specify a different type
if (process.env.JEST_WORKER_ID ?? process.env.NODE_ENV === 'test') {
return jest.requireActual('oidc-provider/lib/shared/check_resource.js');
}
// @ts-expect-error

Check failure on line 32 in src/identity/IdentityUtil.ts

View workflow job for this annotation

GitHub Actions / npm-test / lint

Do not use "@ts-expect-error" because it alters compilation errors
return import('oidc-provider/lib/shared/check_resource.js');

Check failure on line 33 in src/identity/IdentityUtil.ts

View workflow job for this annotation

GitHub Actions / npm-test / lint

Unexpected use of file extension "js" for "oidc-provider/lib/shared/check_resource.js"
}
19 changes: 18 additions & 1 deletion src/identity/configuration/IdentityProviderFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import type { AlgJwk, JwkGenerator } from './JwkGenerator';
import type { PromptFactory } from './PromptFactory';
import type { ProviderFactory } from './ProviderFactory';
import type { JwtAssertionsGrantHandler } from './JwtAssertionsGrantHandler';

export interface IdentityProviderFactoryArgs {
/**
Expand Down Expand Up @@ -75,6 +76,11 @@
* Used to write out errors thrown by the OIDC library.
*/
responseWriter: ResponseWriter;

/**
* Handles JWT Assertion Grant.
*/
jwtAssertionsGrantHandler: JwtAssertionsGrantHandler;
}

const COOKIES_KEY = 'cookie-secret';
Expand All @@ -101,6 +107,7 @@
private readonly showStackTrace: boolean;
private readonly errorHandler: ErrorHandler;
private readonly responseWriter: ResponseWriter;
private readonly jwtAssertionsGrantHandler: JwtAssertionsGrantHandler;

private provider?: Provider;

Expand All @@ -122,6 +129,7 @@
this.showStackTrace = args.showStackTrace;
this.errorHandler = args.errorHandler;
this.responseWriter = args.responseWriter;
this.jwtAssertionsGrantHandler = args.jwtAssertionsGrantHandler;
}

public async getProvider(): Promise<Provider> {
Expand Down Expand Up @@ -162,6 +170,9 @@
// Allow provider to interpret reverse proxy headers.
provider.proxy = true;

// Custom grant type with JWT assertions
provider.registerGrantType(this.jwtAssertionsGrantHandler.grantType, this.jwtAssertionsGrantHandler.handler.bind(this.jwtAssertionsGrantHandler), this.jwtAssertionsGrantHandler.parameters);

Check failure on line 174 in src/identity/configuration/IdentityProviderFactory.ts

View workflow job for this annotation

GitHub Actions / npm-test / lint

This line has a length of 193. Maximum allowed is 120

this.captureErrorResponses(provider);

return provider;
Expand Down Expand Up @@ -276,7 +287,7 @@
// Some fields are still missing, see https://github.com/CommunitySolidServer/CommunitySolidServer/issues/1154#issuecomment-1040233385
config.findAccount = async(ctx: KoaContextWithOIDC, sub: string): Promise<Account> => ({
accountId: sub,
async claims(): Promise<{ sub: string; [key: string]: unknown }> {
async claims(): Promise<{ sub: string;[key: string]: unknown }> {
return { sub, webid: sub, azp: ctx.oidc.client?.clientId };
},
});
Expand All @@ -288,6 +299,12 @@
if (this.isAccessToken(token)) {
return { webid: token.accountId };
}

// {@link JwtAssertionsGrantHandler} passes webid in extra
if (token.extra) {
return token.extra;
}

const clientId = token.client?.clientId;
if (!clientId) {
throw new BadRequestHttpError('Missing client ID from client credentials.');
Expand Down
88 changes: 88 additions & 0 deletions src/identity/configuration/JwtAssertionsGrantHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { importJWK, jwtVerify } from 'jose';
import type {
KoaContextWithOIDC,
} from '../../../templates/types/oidc-provider';
import { importCheckResource, importDpopValidate } from '../IdentityUtil';
import type { JwtAssertionsStore } from '../interaction/jwt-assertions/util/JwtAssertionsStore';
import type { JwkGenerator } from './JwkGenerator';

// Const epochTime = (date = Date.now()) => Math.floor(date / 1000);

export class JwtAssertionsGrantHandler {
public constructor(
private readonly jwkGenerator: JwkGenerator,
private readonly jwtAssertionsStore: JwtAssertionsStore,
) {}

public parameters = [
'client_id',
'scope',
'assertion',
];

public grantType = 'urn:ietf:params:oauth:grant-type:jwt-bearer';

public async handler(ctx: KoaContextWithOIDC) {

Check failure on line 25 in src/identity/configuration/JwtAssertionsGrantHandler.ts

View workflow job for this annotation

GitHub Actions / npm-test / lint

Missing return type on function
const { ClientCredentials, ReplayDetection } = ctx.oidc.provider;

Check failure on line 26 in src/identity/configuration/JwtAssertionsGrantHandler.ts

View workflow job for this annotation

GitHub Actions / npm-test / lint

Variable name `ReplayDetection` must match one of the following formats: camelCase, UPPER_CASE

Check failure on line 26 in src/identity/configuration/JwtAssertionsGrantHandler.ts

View workflow job for this annotation

GitHub Actions / npm-test / lint

Variable name `ClientCredentials` must match one of the following formats: camelCase, UPPER_CASE
const { client } = ctx.oidc;

// Validate DPoP
const dpopValidate = await importDpopValidate().default;
const dPoP = await dpopValidate(ctx);
// TODO
// const unique = await ReplayDetection.unique(client!.clientId, dPoP.jti, epochTime() + 300);
// ctx.assert(unique, new InvalidGrant('DPoP proof JWT Replay detected'));
//

// this is required to trigger resourceIndicators in IdentityProviderFactory
const checkResource = await importCheckResource().default;
await checkResource(ctx, () => {});

// Validate assertion
// TODO: duplicate check if we check verbatim JWT below
const assertion = ctx.oidc.params?.assertion;
if (!assertion) {
// TODO: set correct error resonse
return;
}
const publicKey = await this.jwkGenerator.getPublicKey();
const publicKeyObject = await importJWK(publicKey);

// TODO: better handling of failure
const assertedData = await jwtVerify(assertion as string, publicKeyObject);

// TODO: check revocation
// set correct error
// duplicate check of signature above if we check stored JWT here
const existing = await this.jwtAssertionsStore.findByJwt(assertion as string);
if (!existing) {
return;
}

// Issue access_token
const claims = {
client: client!,
scope: 'webid',
extra: {
webid: assertedData.payload.agent,
},
};
const token = new ClientCredentials(claims);

// @ts-expect-error
token.setThumbprint('jkt', dPoP.thumbprint);

// Somehow related to generating JWT instead of opaque token
token.resourceServer = Object.values(ctx.oidc.resourceServers!)[0];

ctx.oidc.entity('ClientCredentials', token);
const value = await token.save();

ctx.body = {
access_token: value,
expires_in: token.expiration,
token_type: token.tokenType,
scope: token.scope || undefined,
};
}
}
Loading
Loading