Skip to content

Commit

Permalink
refactor(oauth): handle authorization_details (#106)
Browse files Browse the repository at this point in the history
* refactor(oauth): first draft with authorization_details

* fix(oauth): fixed verifyCredentialId

* feat(oauth): verifyAuhtorizationDetails checks mandatory claims

* feat(oauth): new statement for getClaimsFromToken
  • Loading branch information
RebeccaSelvaggini authored Mar 15, 2024
1 parent 4c05178 commit f2b4588
Show file tree
Hide file tree
Showing 5 changed files with 217 additions and 74 deletions.
10 changes: 6 additions & 4 deletions pkg/oauth/src/authenticateHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,11 +91,13 @@ export class AuthenticateHandler {
}

const scope = request.body.scope;
const resource = request.body.resource;
if (!resource) throw new Error('Request is missing resource parameter');
if(scope) {
const resource = request.body.resource;
if (!resource) throw new Error('Request is missing resource parameter');

const valid_scope = await this.verifyScope(scope, resource);
if (!valid_scope) throw new Error('Given scope is not valid');
const valid_scope = await this.verifyScope(scope, resource);
if (!valid_scope) throw new Error('Given scope is not valid');
}

const auth_url = this.authenticationUrl;
const url = auth_url + cl_id;
Expand Down
68 changes: 53 additions & 15 deletions pkg/oauth/src/authorizeHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,8 @@ export class AuthorizeHandler {
);
}

if(!request.body.request_uri) throw new InvalidRequestError("Missing parameter: request_uri");
if(!request.body.client_id) throw new InvalidRequestError("Missing parameter: client_id");
if (!request.body.request_uri) throw new InvalidRequestError("Missing parameter: request_uri");
if (!request.body.client_id) throw new InvalidRequestError("Missing parameter: client_id");

const base_uri = "urn:ietf:params:oauth:request_uri:";
let rand_uri = request.body.request_uri;
Expand All @@ -106,16 +106,16 @@ export class AuthorizeHandler {
//TODO: check if we can convert timestamp in a better way
const timestamp = Math.round(new Date(rand_uri.substring(0, 10)).getTime() / 1000);
const time_now = Math.round(Date.now() / 1000);
if(time_now - timestamp > par_expires_in) {
if (time_now - timestamp > par_expires_in) {
this.model.revokeAuthCodeFromUri(rand_uri, true);
throw new InvalidRequestError(`'${request.body.request_uri}' has expired`);
}

const client = await this.getClient(request);
if(!client) throw new InvalidClientError(`Failed to get Client from '${request.body.client_id}'`);
if (!client) throw new InvalidClientError(`Failed to get Client from '${request.body.client_id}'`);

const code = this.getAuthorizationCode(rand_uri);
if(!code) throw new Error(`Failed to get Authorization Code from '${request.body.request_uri}'`);
if (!code) throw new Error(`Failed to get Authorization Code from '${request.body.request_uri}'`);
this.model.revokeAuthCodeFromUri(rand_uri);

return code;
Expand All @@ -142,16 +142,29 @@ export class AuthorizeHandler {
);
}

if(expires_in) par_expires_in = expires_in;
if (expires_in) par_expires_in = expires_in;

if(request.body.request_uri) throw new InvalidRequestError("Found request_uri parameter in the request");
if (request.body.request_uri) throw new InvalidRequestError("Found request_uri parameter in the request");

const expiresAt = this.getAuthorizationCodeLifetime();
const client = await this.getClient(request);
const user = await this.getUser(request, response);

if (!user) throw new UnauthorizedClientError("Client authentication failed");

if (request.body.authorization_details) {
const auth_det = JSON.parse(request.body.authorization_details);
var authorization_details = await this.verifyAuthrizationDetails(auth_det);
if (authorization_details.length === 0) throw new OAuthError("Given authorization_details are not valid");
var validScope: string[] = [authorization_details[0]['credential_configuration_id']];
}
else {
const resource = request.body.resource;
const requestedScope = this.getScope(request);
if (!requestedScope) throw new InvalidRequestError("Neither authorization_details, nor scope parameter found in request");
var validScope = await this.validateScope(user, client, requestedScope, resource);
}

let uri;
let state;

Expand All @@ -164,9 +177,6 @@ export class AuthorizeHandler {
}
}

const resource = request.body.resource;
const requestedScope = this.getScope(request);
var validScope = await this.validateScope(user, client, requestedScope!, resource);
const authorizationCode = await this.generateAuthorizationCode(client);

const ResponseType = this.getResponseType(request);
Expand All @@ -186,15 +196,16 @@ export class AuthorizeHandler {
user,
codeChallenge,
codeChallengeMethod,
authorization_details,
rand_uri
);
if(!code) { throw Error("Failed to create the Authorization Code"); }
if (!code) { throw Error("Failed to create the Authorization Code"); }

const responseTypeInstance = new ResponseType(code.authorizationCode);
const redirectUri = this.buildSuccessRedirectUri(uri, responseTypeInstance);
this.updateResponse(response, redirectUri, state);

return { request_uri: base_uri.concat(rand_uri), expires_in: par_expires_in}
return { request_uri: base_uri.concat(rand_uri), expires_in: par_expires_in }
} catch (err) {
let e = err;

Expand Down Expand Up @@ -285,10 +296,34 @@ export class AuthorizeHandler {
return client;
}

async verifyAuthrizationDetails(authorization_details: { [key: string]: any }[]) {
const verifiedAuthDetails: any = [];
await Promise.all(authorization_details.map(async (dict: { [key: string]: any }) => {

if (!dict['type']) throw new OAuthError("Invalid authorization_details: missing parameter type");
if (!dict['locations']) throw new OAuthError("Invalid authorization_details: missing parameter locations");
if (!dict['credential_configuration_id']) throw new OAuthError("Invalid authorization_details: missing parameter credential_configuration_id");

const verified_credentials = await this.model.verifyCredentialId(dict['credential_configuration_id'], dict['locations'][0]);
if (verified_credentials.valid_credentials.length == 0) throw new OAuthError(`Invalid authorization_details: '${dict['credential_configuration_id']}' is not a valid credential_id `)

const claims = verified_credentials.credential_claims.get(dict['credential_configuration_id']);
claims!.map((claim: string) => {
if (!dict[claim]) throw new OAuthError(`Invalid authorization_details: missing parameter '${claim}'`);
});

// TODO: verify content of authorization_details claims

verifiedAuthDetails.push(dict);
}));

return verifiedAuthDetails;
}

/**
* Validate requested scope.
*/
async validateScope (user:User, client:Client, scope:string[], resource:string) {
async validateScope(user: User, client: Client, scope: string[], resource: string) {
if (this.model.validateScope) {
const validatedScope = await this.model.validateScope(user, client, scope, resource);

Expand Down Expand Up @@ -360,7 +395,7 @@ export class AuthorizeHandler {
* Save authorization code.
*/

async saveAuthorizationCode(authorizationCode: string, expiresAt: Date, redirectUri: string, scope: string[], client: Client, user: User, codeChallenge: string, codeChallengeMethod: string, rand_uri: string) {
async saveAuthorizationCode(authorizationCode: string, expiresAt: Date, redirectUri: string, scope: string[], client: Client, user: User, codeChallenge: string, codeChallengeMethod: string, authorization_details: { [key: string]: any }[], rand_uri: string) {
let code: AuthorizationCode = {
authorizationCode: authorizationCode,
expiresAt: expiresAt,
Expand All @@ -373,8 +408,11 @@ export class AuthorizeHandler {
code.codeChallenge = codeChallenge;
code.codeChallengeMethod = codeChallengeMethod;
}
if (authorization_details) {
code['authorization_details'] = authorization_details;
}

return this.model.saveAuthorizationCode(code, client, user, rand_uri);
return this.model.saveAuthorizationCode(code, client, user, authorization_details, rand_uri);
}

async validateRedirectUri(redirectUri: string, client: Client) {
Expand Down
104 changes: 76 additions & 28 deletions pkg/oauth/src/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
Client,
Falsey,
InsufficientScopeError,
InvalidTokenError,
Request,
Token,
User,
Expand Down Expand Up @@ -33,6 +34,7 @@ export class InMemoryCache implements AuthorizationCodeModel {
users: User[];
codes: AuthorizationCode[];
uri_codes: Map<string, AuthorizationCode>;
authorization_details: Map<string, { [key: string]: any }[]>;
serverData: { jwk: JWK, url: string };
options: JsonableObject;
dpop_jwks: { [key: string]: any }[];
Expand All @@ -52,6 +54,7 @@ export class InMemoryCache implements AuthorizationCodeModel {
this.tokens = [];
this.codes = [];
this.uri_codes = new Map();
this.authorization_details = new Map();
this.dpop_jwks = [];
}

Expand Down Expand Up @@ -152,6 +155,15 @@ export class InMemoryCache implements AuthorizationCodeModel {
return Promise.resolve(codeSaved);
}

getAuthorizationDetails(authorizationCode: string) {
const auth_details = this.authorization_details.get(authorizationCode);
return auth_details;
}

revokeAuthorizationDetails(authorizationCode: string) {
this.authorization_details.delete(authorizationCode);
}

/**
* Invoked to retrieve an existing authorization code from this.codes.
*
Expand All @@ -169,14 +181,14 @@ export class InMemoryCache implements AuthorizationCodeModel {
*/
getAuthCodeFromUri(rand_uri: string) {
const code = this.uri_codes.get(rand_uri);
if(!code) throw new OAuthError("Failed to get Authorization Code: given request_uri is not valid");
if (!code) throw new OAuthError("Failed to get Authorization Code: given request_uri is not valid");
return code;
}

revokeAuthCodeFromUri(rand_uri: string, expired?: boolean) {
const code = this.uri_codes.get(rand_uri);
if(!code) throw new OAuthError("Authorization code does not exist on server");
if(!expired) {
if (!code) throw new OAuthError("Authorization code does not exist on server");
if (!expired) {
this.codes.push(code);
}
this.uri_codes.delete(rand_uri);
Expand All @@ -186,7 +198,7 @@ export class InMemoryCache implements AuthorizationCodeModel {
* Invoked to save an authorization code.
*
*/
saveAuthorizationCode(code: Pick<AuthorizationCode, "authorizationCode" | "expiresAt" | "redirectUri" | "scope" | "codeChallenge" | "codeChallengeMethod">, client: Client, user: User, rand_uri?: string): Promise<Falsey | AuthorizationCode> {
saveAuthorizationCode(code: Pick<AuthorizationCode, "authorizationCode" | "expiresAt" | "redirectUri" | "scope" | "codeChallenge" | "codeChallengeMethod">, client: Client, user: User, authorization_details?: { [key: string]: any }[], rand_uri?: string): Promise<Falsey | AuthorizationCode> {
let codeSaved: AuthorizationCode = {
authorizationCode: code.authorizationCode,
expiresAt: code.expiresAt,
Expand All @@ -200,8 +212,11 @@ export class InMemoryCache implements AuthorizationCodeModel {
codeSaved.codeChallenge = code.codeChallenge
codeSaved.codeChallengeMethod = code.codeChallengeMethod
}
if (authorization_details) {
this.authorization_details.set(code.authorizationCode, authorization_details);
}
//TODO: check this
if(rand_uri) {
if (rand_uri) {
this.uri_codes.set(rand_uri, codeSaved);
} else {
this.codes.push(codeSaved);
Expand Down Expand Up @@ -278,7 +293,10 @@ export class InMemoryCache implements AuthorizationCodeModel {
tokenSaved['resource'] = client['resource'];
}
}

if (token['authorizationCode']) {
const auth_details = this.authorization_details.get(token['authorizationCode']);
if (auth_details) tokenSaved['authorization_details'] = auth_details;
}
tokenSaved['c_nonce'] = randomBytes(20).toString('hex');
tokenSaved['c_nonce_expires_in'] = 60 * 60;

Expand All @@ -288,7 +306,6 @@ export class InMemoryCache implements AuthorizationCodeModel {
tokenSaved['jkt'] = this.createJWKThumbprint(dpop_jwk['jwk']);
}
if (this.options && this.options['allowExtendedTokenAttributes']) {
//TODO: problem with authorization_details
var keys = Object.keys(token);
keys.forEach((key: string) => {
if (!tokenSaved[key]) {
Expand All @@ -312,17 +329,18 @@ export class InMemoryCache implements AuthorizationCodeModel {
if (this.serverData.jwk == null) throw new OAuthError("Missing server private JWK");
let privateKey = await importJWK(this.serverData.jwk);
let alg = this.serverData.jwk.alg || 'ES256';
let public_jwk:JWK = {
let public_jwk: JWK = {
kty: this.serverData.jwk.kty!,
x: this.serverData.jwk.x!,
y: this.serverData.jwk.y!,
crv: this.serverData.jwk.crv!
}

const jws = new SignJWT({ sub: randomBytes(20).toString('hex') })
.setProtectedHeader({ alg: alg,
jwk: public_jwk
})
.setProtectedHeader({
alg: alg,
jwk: public_jwk
})
.setIssuedAt(Math.round(Date.now() / 1000))
.setIssuer(this.serverData.url)
.setAudience(clientId)
Expand Down Expand Up @@ -428,21 +446,8 @@ export class InMemoryCache implements AuthorizationCodeModel {
return Promise.resolve(true);
}


async validateScope?(user: User, client: Client, scope?: string[] | undefined, resource?: string): Promise<Falsey | string[]> {

if (!user || !client) throw new OAuthError("Invalid input parameters for ValidateScope");

if (!scope) {
throw new InsufficientScopeError(
'Insufficient scope: authorized scope is insufficient',
);
}
if (!resource) {
var resource = client['resource'] as string | undefined;
if (!resource)
throw new OAuthError('Invalid request: needed resource to verify scope');
}
async verifyCredentialId(scope: string, resource: string) {
if (resource.slice(-1) === "/") resource = resource.slice(0, -1);
const url = resource + '/.well-known/openid-credential-issuer';
const response = await fetch(url);
if (!response.ok) {
Expand All @@ -451,23 +456,66 @@ export class InMemoryCache implements AuthorizationCodeModel {
const result = await response.json();
const credentials_supported = result.credential_configurations_supported;
var valid_credentials = [];
var credential_claims = new Map<string, string[]>();

for (var key in credentials_supported) {
const type_arr = credentials_supported[key].credential_definition.type;
if (
type_arr.find((id: any) => {
return id === scope[0];
return id === scope;
}) != undefined
) {
valid_credentials.push(scope);
const credentialSubject = credentials_supported[key].credential_definition.credentialSubject;
var claims = [];
for (var claim in credentialSubject) {
if (credentialSubject[claim].mandatory) claims.push(claim);
}
credential_claims.set(scope, claims);
break;
}
}

if (valid_credentials.length > 0) return Promise.resolve(scope);
return { valid_credentials: valid_credentials, credential_claims: credential_claims };
}

async validateScope?(user: User, client: Client, scope?: string[] | undefined, resource?: string): Promise<Falsey | string[]> {

if (!user || !client) throw new OAuthError("Invalid input parameters for ValidateScope");

if (!scope) {
throw new InsufficientScopeError(
'Insufficient scope: authorized scope is insufficient',
);
}
if (!resource) {
var resource = client['resource'] as string | undefined;
if (!resource)
throw new OAuthError('Invalid request: needed resource to verify scope');
}

var verified_credentials = await this.verifyCredentialId(scope[0]!, resource);

if (verified_credentials.valid_credentials.length > 0) return Promise.resolve(scope);
else return false;

}

async getClaimsFromToken(accessToken: string) {
const token = await this.getAccessToken(accessToken);
if (!token) throw new InvalidTokenError("Given token is not valid");
const auth_details = token['authorization_details'];
if (!auth_details) throw new InvalidTokenError("authorization_details not found in accessToken");
var claims: { [key: string]: any }[] = [];
auth_details.map((dict: { [key: string]: any }) => {
delete dict['type'];
delete dict['locations'];
delete dict['credential_configuration_id'];
claims.push(dict);
});

return claims;
}
}

// generateRefreshToken?(client: Client, user: User, scope: string[]): Promise<string> {
Expand Down
Loading

0 comments on commit f2b4588

Please sign in to comment.