actually implement oauth2 pcke, drop image scope

This commit is contained in:
Evert Prants 2022-12-03 10:02:58 +02:00
parent 4d6267d40a
commit c3c297a9a5
Signed by: evert
GPG Key ID: 1688DA83D222D0B5
11 changed files with 66 additions and 29 deletions

14
package-lock.json generated
View File

@ -9,7 +9,7 @@
"version": "0.0.1", "version": "0.0.1",
"license": "UNLICENSED", "license": "UNLICENSED",
"dependencies": { "dependencies": {
"@icynet/oauth2-provider": "^1.0.6", "@icynet/oauth2-provider": "^1.0.7",
"@nestjs/common": "^9.0.11", "@nestjs/common": "^9.0.11",
"@nestjs/core": "^9.0.11", "@nestjs/core": "^9.0.11",
"@nestjs/platform-express": "^9.0.11", "@nestjs/platform-express": "^9.0.11",
@ -2142,9 +2142,9 @@
"dev": true "dev": true
}, },
"node_modules/@icynet/oauth2-provider": { "node_modules/@icynet/oauth2-provider": {
"version": "1.0.6", "version": "1.0.7",
"resolved": "https://registry.npmjs.org/@icynet/oauth2-provider/-/oauth2-provider-1.0.6.tgz", "resolved": "https://registry.npmjs.org/@icynet/oauth2-provider/-/oauth2-provider-1.0.7.tgz",
"integrity": "sha512-CsPQZB0Jbzxll4re34aPtZFVNkeeWtC4aW9UZCg8U57fPL8/Xe/2dfnigxdn4r9jVdd6d/qiFSh5x7wrFUmYrw==", "integrity": "sha512-YdzkB8c/7BOUZaiKpeEFbLfttfH6kztDm+qUG3zqgZ6J+CXJMqtLnJtFu++bn8/okYikU1ErdZq2/4fetD1C+Q==",
"dependencies": { "dependencies": {
"express": "^4.17.3", "express": "^4.17.3",
"express-session": "^1.17.2" "express-session": "^1.17.2"
@ -14155,9 +14155,9 @@
"dev": true "dev": true
}, },
"@icynet/oauth2-provider": { "@icynet/oauth2-provider": {
"version": "1.0.6", "version": "1.0.7",
"resolved": "https://registry.npmjs.org/@icynet/oauth2-provider/-/oauth2-provider-1.0.6.tgz", "resolved": "https://registry.npmjs.org/@icynet/oauth2-provider/-/oauth2-provider-1.0.7.tgz",
"integrity": "sha512-CsPQZB0Jbzxll4re34aPtZFVNkeeWtC4aW9UZCg8U57fPL8/Xe/2dfnigxdn4r9jVdd6d/qiFSh5x7wrFUmYrw==", "integrity": "sha512-YdzkB8c/7BOUZaiKpeEFbLfttfH6kztDm+qUG3zqgZ6J+CXJMqtLnJtFu++bn8/okYikU1ErdZq2/4fetD1C+Q==",
"requires": { "requires": {
"express": "^4.17.3", "express": "^4.17.3",
"express-session": "^1.17.2" "express-session": "^1.17.2"

View File

@ -24,7 +24,7 @@
"test:e2e": "jest --config ./test/jest-e2e.json" "test:e2e": "jest --config ./test/jest-e2e.json"
}, },
"dependencies": { "dependencies": {
"@icynet/oauth2-provider": "^1.0.6", "@icynet/oauth2-provider": "^1.0.7",
"@nestjs/common": "^9.0.11", "@nestjs/common": "^9.0.11",
"@nestjs/core": "^9.0.11", "@nestjs/core": "^9.0.11",
"@nestjs/platform-express": "^9.0.11", "@nestjs/platform-express": "^9.0.11",

View File

@ -24,7 +24,7 @@ export class AppController {
response_types_supported: ['code', 'id_token'], response_types_supported: ['code', 'id_token'],
id_token_signing_alg_values_supported: [this.config.get('jwt.algorithm')], id_token_signing_alg_values_supported: [this.config.get('jwt.algorithm')],
subject_types_supported: ['public'], subject_types_supported: ['public'],
scopes_supported: ['openid', 'profile', 'email'], scopes_supported: ['openid', 'profile', 'picture', 'email'],
claims_supported: [ claims_supported: [
'aud', 'aud',
'exp', 'exp',
@ -33,6 +33,7 @@ export class AppController {
'sub', 'sub',
'name', 'name',
'preferred_username', 'preferred_username',
'nickname',
'profile', 'profile',
'picture', 'picture',
'updated_at', 'updated_at',

View File

@ -0,0 +1,17 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class pcke1670052416869 implements MigrationInterface {
name = 'pcke1670052416869';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE \`o_auth2_token\` ADD \`pcke\` text NULL`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE \`o_auth2_token\` DROP COLUMN \`pcke\``,
);
}
}

View File

@ -73,16 +73,11 @@ export class ApiController {
userData.email_verified = true; userData.email_verified = true;
} }
if ( if ((scope.includes('picture') || scopelessAccess) && user.picture) {
(scope.includes('image') || userData.picture = `${this._config.get('app.base_url')}/uploads/${
scope.includes('picture') ||
scopelessAccess) &&
user.picture
) {
userData.image = `${this._config.get('app.base_url')}/uploads/${
user.picture.file user.picture.file
}`; }`;
userData.image_file = user.picture.file; userData.picture_file = user.picture.file;
} }
if ( if (

View File

@ -6,6 +6,7 @@ export class CodeAdapter implements OAuth2CodeAdapter {
constructor(private _service: OAuth2Service) {} constructor(private _service: OAuth2Service) {}
ttl = 3600; ttl = 3600;
challengeMethods = ['plain', 'S256'];
async create( async create(
userId: number, userId: number,
@ -13,6 +14,8 @@ export class CodeAdapter implements OAuth2CodeAdapter {
scope: string | string[], scope: string | string[],
ttl: number, ttl: number,
nonce?: string, nonce?: string,
codeChallenge?: string,
codeChallengeMethod?: 'plain' | 'S256',
): Promise<string> { ): Promise<string> {
const client = await this._service.clientService.getById(clientId); const client = await this._service.clientService.getById(clientId);
const user = await this._service.userService.getById(userId); const user = await this._service.userService.getById(userId);
@ -24,6 +27,12 @@ export class CodeAdapter implements OAuth2CodeAdapter {
).join(' '); ).join(' ');
const expiresAt = new Date(Date.now() + ttl * 1000); const expiresAt = new Date(Date.now() + ttl * 1000);
const pcke =
codeChallenge && codeChallengeMethod
? `${this.challengeMethods.indexOf(
codeChallengeMethod,
)}:${codeChallenge}`
: null;
this._service.tokenService.insertToken( this._service.tokenService.insertToken(
accessToken, accessToken,
@ -33,6 +42,7 @@ export class CodeAdapter implements OAuth2CodeAdapter {
expiresAt, expiresAt,
user, user,
nonce, nonce,
pcke,
); );
return accessToken; return accessToken;
@ -49,11 +59,22 @@ export class CodeAdapter implements OAuth2CodeAdapter {
return null; return null;
} }
let codeChallenge: string;
let codeChallengeMethod: 'plain' | 'S256';
if (find.pcke) {
codeChallengeMethod = this.challengeMethods[
Number(find.pcke.substring(0, 1))
] as 'plain' | 'S256';
codeChallenge = find.pcke.substring(2);
}
return { return {
...find, ...find,
code: find.token, code: find.token,
client_id: find.client.client_id, client_id: find.client.client_id,
user_id: find.user.id, user_id: find.user.id,
code_challenge: codeChallenge,
code_challenge_method: codeChallengeMethod,
}; };
} }
@ -82,4 +103,11 @@ export class CodeAdapter implements OAuth2CodeAdapter {
checkTTL(code: OAuth2Code): boolean { checkTTL(code: OAuth2Code): boolean {
return code.expires_at.getTime() > Date.now(); return code.expires_at.getTime() > Date.now();
} }
getCodeChallenge(code: OAuth2Code) {
return {
method: code.code_challenge_method,
challenge: code.code_challenge,
};
}
} }

View File

@ -25,12 +25,7 @@ export class IcyJWTAdapter implements JWTAdapter {
userData.email_verified = true; userData.email_verified = true;
} }
if ( if (scope.includes('picture') && user.picture) {
(scope.includes('image') ||
scope.includes('picture') ||
scope.includes('profile')) &&
user.picture
) {
userData.picture = `${this._client.config.get('app.base_url')}/uploads/${ userData.picture = `${this._client.config.get('app.base_url')}/uploads/${
user.picture.file user.picture.file
}`; }`;

View File

@ -15,7 +15,7 @@ import { UserAdapter } from './adapter/user.adapter';
const SCOPE_DESCRIPTION: Record<string, string> = { const SCOPE_DESCRIPTION: Record<string, string> = {
email: 'Email address', email: 'Email address',
image: 'Profile picture', picture: 'Profile picture',
}; };
const ALWAYS_AVAILABLE = ['Username and display name']; const ALWAYS_AVAILABLE = ['Username and display name'];
@ -40,10 +40,7 @@ export class OAuth2Service {
let disallowedScopes = [...ALWAYS_UNAVAILABLE]; let disallowedScopes = [...ALWAYS_UNAVAILABLE];
Object.keys(SCOPE_DESCRIPTION).forEach((item) => { Object.keys(SCOPE_DESCRIPTION).forEach((item) => {
if ( if (scope.includes(item)) {
scope.includes(item) ||
(item === 'image' && scope.includes('picture'))
) {
allowedScopes.push(SCOPE_DESCRIPTION[item]); allowedScopes.push(SCOPE_DESCRIPTION[item]);
} else { } else {
disallowedScopes.push(SCOPE_DESCRIPTION[item]); disallowedScopes.push(SCOPE_DESCRIPTION[item]);

View File

@ -21,7 +21,6 @@ export class OAuth2ClientService {
]; ];
public availableScopes = [ public availableScopes = [
'image',
'picture', 'picture',
'profile', 'profile',
'email', 'email',

View File

@ -29,6 +29,9 @@ export class OAuth2Token {
@Column({ nullable: true, type: 'text' }) @Column({ nullable: true, type: 'text' })
nonce: string; nonce: string;
@Column({ nullable: true, type: 'text' })
pcke: string;
@Column({ type: 'text', nullable: true }) @Column({ type: 'text', nullable: true })
scope: string; scope: string;

View File

@ -19,6 +19,7 @@ export class OAuth2TokenService {
expiry: Date, expiry: Date,
user?: User, user?: User,
nonce?: string, nonce?: string,
pcke?: string,
): Promise<OAuth2Token> { ): Promise<OAuth2Token> {
const newToken = new OAuth2Token(); const newToken = new OAuth2Token();
newToken.client = client; newToken.client = client;
@ -28,6 +29,7 @@ export class OAuth2TokenService {
newToken.user = user; newToken.user = user;
newToken.expires_at = expiry; newToken.expires_at = expiry;
newToken.nonce = nonce; newToken.nonce = nonce;
newToken.pcke = pcke;
await this.tokenRepository.save(newToken); await this.tokenRepository.save(newToken);