actually implement oauth2 pcke, drop image scope
This commit is contained in:
parent
4d6267d40a
commit
c3c297a9a5
14
package-lock.json
generated
14
package-lock.json
generated
@ -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"
|
||||||
|
@ -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",
|
||||||
|
@ -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',
|
||||||
|
17
src/migration/1670052416869-pcke.ts
Normal file
17
src/migration/1670052416869-pcke.ts
Normal 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\``,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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 (
|
||||||
|
@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}`;
|
}`;
|
||||||
|
@ -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]);
|
||||||
|
@ -21,7 +21,6 @@ export class OAuth2ClientService {
|
|||||||
];
|
];
|
||||||
|
|
||||||
public availableScopes = [
|
public availableScopes = [
|
||||||
'image',
|
|
||||||
'picture',
|
'picture',
|
||||||
'profile',
|
'profile',
|
||||||
'email',
|
'email',
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
@ -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);
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user