Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(oauth): new statements for authorization_details and request_uri #148

Merged
merged 2 commits into from
Jun 6, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
48 changes: 31 additions & 17 deletions pkg/oauth/src/authorizeHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,31 +83,19 @@ export class AuthorizeHandler {
this.model = options.model;
}

/**
* Authorize Handler.
*/
// handle /authorize request containing request_uri and client_id
async handle(request: Request, response: Response) {
async verifyAuthorizeParams(request: Request) {
if (!(request instanceof Request)) {
throw new InvalidArgumentError(
'Invalid argument: `request` must be an instance of Request',
);
}

if (!(response instanceof Response)) {
throw new InvalidArgumentError(
'Invalid argument: `response` must be an instance of Response',
);
}

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;
rand_uri = rand_uri.replace(base_uri, "");

//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) {
Expand All @@ -118,6 +106,32 @@ export class AuthorizeHandler {
const client = await this.getClient(request);
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(`request_uri '${request.body.request_uri}' is not valid`);
return;
}

/**
* Authorize Handler.
*/
// handle /authorize request containing request_uri and client_id
async handle(request: Request, response: Response) {
if (!(request instanceof Request)) {
throw new InvalidArgumentError(
'Invalid argument: `request` must be an instance of Request',
);
}

if (!(response instanceof Response)) {
throw new InvalidArgumentError(
'Invalid argument: `response` must be an instance of Response',
);
}

const base_uri = "urn:ietf:params:oauth:request_uri:";
let rand_uri = request.body.request_uri;
rand_uri = rand_uri.replace(base_uri, "");

const code = this.getAuthorizationCode(rand_uri);
if (!code) throw new Error(`Failed to get Authorization Code from '${request.body.request_uri}'`);
this.model.revokeAuthCodeFromUri(rand_uri);
Expand Down Expand Up @@ -311,10 +325,10 @@ export class AuthorizeHandler {
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}'`);
});
// 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

Expand Down
14 changes: 14 additions & 0 deletions pkg/oauth/src/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,20 @@ export class InMemoryCache implements AuthorizationCodeModel {

getAuthorizationDetails(authorizationCode: string) {
const auth_details = this.authorization_details.get(authorizationCode);
if (!auth_details) throw new OAuthError("Failed to get authorization_details: given authorization_code is not valid");
return auth_details;
}

updateAuthorizationDetails(request_uri: string, data: any) {
const base_uri = "urn:ietf:params:oauth:request_uri:";
const rand_uri = request_uri.replace(base_uri, "");
const auth_code = this.getAuthCodeFromUri(rand_uri);
let auth_details = this.getAuthorizationDetails(auth_code.authorizationCode);
//TODO: case of multiple elem in auth_details
if (auth_details[0]) {
auth_details[0]['claims'] = data;
}
this.authorization_details.set(auth_code.authorizationCode, auth_details);
return auth_details;
}

Expand Down
79 changes: 79 additions & 0 deletions pkg/oauth/src/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,53 @@ export const createToken = p.new(
},
);

/**
* @internal
*/
// Sentence that allows to verify the parameters of the /authorize request
export const verifyRequestUri = p.new(
['request', 'server_data'],
'verify request parameters',
async (ctx) => {
const params = ctx.fetch('request') as JsonableObject;
const body = params['body'];
const headers = params['headers'];
if (!body || !headers) return ctx.fail(new OauthError("Input request is not valid"));
if (typeof body !== 'string') return ctx.fail(new OauthError("Request body must be a string"));
const serverData = ctx.fetch('server_data') as { jwk: JWK, url: string, authenticationUrl: string };
if (!serverData['jwk'] || !serverData['url']) return ctx.fail(new OauthError("Server data is missing some parameters"));

const request = new Request({
body: parseQueryStringToDictionary(body),
headers: headers,
method: 'GET',
query: {},
});

const options = {
accessTokenLifetime: 60 * 60, // 1 hour.
refreshTokenLifetime: 60 * 60 * 24 * 14, // 2 weeks.
allowExtendedTokenAttributes: true,
requireClientAuthentication: {}, // defaults to true for all grant types
};

const model = getInMemoryCache(serverData, options);
try {
const handler = getAuthenticateHandler(model, serverData.authenticationUrl);
const authorize_options = {
model: model,
authenticateHandler: handler,
allowEmptyState: false,
authorizationCodeLifetime: 5 * 60 // 5 minutes.
}
await new AuthorizeHandler(authorize_options).verifyAuthorizeParams(request);
} catch(e) {
return ctx.fail(new OauthError(e.message));
}
return ctx.pass("Given request_uri and client_id are valid");
},
);

/**
* @internal
*/
Expand Down Expand Up @@ -275,4 +322,36 @@ export const getClaims = p.new(
},
);

/**
* @internal
*/
// Sentence that allows to add a string dict(?) to the authorization_details binded to the given request_uri
export const changeAuthDetails = p.new(
['request_uri', 'data', 'server_data'],
'add data to authorization_details',
async (ctx) => {
const params = ctx.fetch('data') as JsonableObject;
const uri = ctx.fetch('request_uri') as string;
const serverData = ctx.fetch('server_data') as { jwk: JWK, url: string, authenticationUrl: string };
if (!serverData['jwk'] || !serverData['url']) return ctx.fail(new OauthError("Server data is missing some parameters"));

const options = {
accessTokenLifetime: 60 * 60, // 1 hour.
refreshTokenLifetime: 60 * 60 * 24 * 14, // 2 weeks.
allowExtendedTokenAttributes: true,
requireClientAuthentication: {}, // defaults to true for all grant types
};

const model = getInMemoryCache(serverData, options);
let res
try {
res = await model.updateAuthorizationDetails(uri, params);
} catch(e) {
return ctx.fail(new OauthError(e.message));
}
return ctx.pass(res);
},
);


export const oauth = p;
26 changes: 19 additions & 7 deletions pkg/oauth/test/e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { Slangroom } from '@slangroom/core';
import { oauth } from '@slangroom/oauth';
import { SignJWT, importJWK } from 'jose';
import { randomBytes } from 'crypto';
import { JsonableObject } from '@slangroom/shared';
import { JsonableObject, JsonableArray } from '@slangroom/shared';

//For reference see Section 4 of https://datatracker.ietf.org/doc/html/rfc9449.html
async function create_dpop_proof() {
Expand Down Expand Up @@ -75,8 +75,7 @@ Then print data
},
request: {
//&scope=Auth1&resource=http%3A%2F%2Fissuer1.zenswarm.forkbomb.eu%3A3100%2Fcredential_issuer%2F
//authorization_details=%5B%7B%22type%22%3A%20%22openid_credential%22%2C%20%22credential_configuration_id%22%3A%20%22Auth1%22%2C%22locations%22%3A%20%5B%22http%3A%2F%2Fissuer1.zenswarm.forkbomb.eu%3A3100%2Fcredential_issuer%2F%22%5D%7D%5D
body: 'response_type=code&client_id=did:dyne:sandbox.genericissuer:6Cp8mPUvJmQaMxQPSnNyhb74f9Ga4WqfXCkBneFgikm5&state=xyz&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM&code_challenge_method=S256&redirect_uri=https%3A%2F%2FWallet.example.org%2Fcb&authorization_details=%5B%7B%22type%22%3A+%22openid_credential%22%2C+%22credential_configuration_id%22%3A+%22Auth1%22%2C%22locations%22%3A+%5B%22http%3A%2F%2Fissuer1.zenswarm.forkbomb.eu%3A3100%2Fcredential_issuer%2F%22%5D%2C%22given_name%22%3A%22Pippo%22%2C+%22family_name%22%3A%22Peppe%22%2C%22is_human%22%3Atrue%7D%5D',
body: 'response_type=code&client_id=did:dyne:sandbox.genericissuer:6Cp8mPUvJmQaMxQPSnNyhb74f9Ga4WqfXCkBneFgikm5&state=xyz&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM&code_challenge_method=S256&redirect_uri=https%3A%2F%2FWallet.example.org%2Fcb&authorization_details=%5B%7B%22type%22%3A+%22openid_credential%22%2C+%22credential_configuration_id%22%3A+%22Auth1%22%2C%22locations%22%3A+%5B%22http%3A%2F%2Fissuer1.zenswarm.forkbomb.eu%3A3100%2Fcredential_issuer%2F%22%5D%7D%5D',
headers: {
Authorization: '',
},
Expand All @@ -93,7 +92,7 @@ Then print data
},
},
});
//console.log(res.result['request_uri_out']);

t.truthy(res.result['request_uri_out']);

const scriptCreateBodyRequest1 = `
Expand All @@ -105,8 +104,10 @@ Given I have a 'string dictionary' named 'request_uri_out'
When I create the copy of 'request_uri' from dictionary 'request_uri_out'
# TODO: check if we need encoding before append
When I append 'copy' to 'body'
When I rename 'copy' to 'request_uri'

Then print the 'body'
Then print the 'request_uri'
`;
const resb = await slangroom.execute(scriptCreateBodyRequest1, {
keys: {
Expand All @@ -119,8 +120,11 @@ Then print the 'body'
const scriptAuthCode = `
Rule unknown ignore

Given I send request 'request' and send server_data 'server' and verify request parameters
Given I send request_uri 'request_uri' and send data 'data' and send server_data 'server' and add data to authorization_details and output into 'auth_details'
Given I send request 'request' and send server_data 'server' and generate authorization code and output into 'authCode'

Given I have a 'string array' named 'auth_details'
Given I have a 'string dictionary' named 'authCode'

Then print data
Expand All @@ -144,11 +148,19 @@ Then print data
headers: {
Authorization: '',
},
},
request_uri: resb.result['request_uri'] || '',
data: {
email_address: '[email protected]'
}
},
});
//console.log(res_auth.result['authCode']);

t.truthy(res_auth.result['authCode']);
t.truthy(res_auth.result['auth_details']);
let authDet = res_auth.result['auth_details']! as JsonableArray;
let authDet_dict = authDet[0]! as JsonableObject;
t.truthy(authDet_dict['claims']);

const scriptCreateBodyRequest = `
Rule unknown ignore
Expand Down Expand Up @@ -202,7 +214,7 @@ Then print data
},
},
});
//console.log(res3.result['accessToken_jwt']);

t.truthy(res3.result['accessToken_jwt']);
const scriptGetClaims = `
Rule unknown ignore
Expand All @@ -223,7 +235,7 @@ Then print data
token: token_str
},
});
//console.log(res4.result['claims']);

t.truthy(res4.result['claims']);

});
Expand Down
Loading