You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
tailchat/server/services/openapi/oidc/adapter.ts

176 lines
4.2 KiB
TypeScript

import type { Adapter, AdapterPayload } from 'oidc-provider';
import { config } from 'tailchat-server-sdk';
import RedisClient from 'ioredis';
import _ from 'lodash';
import { OpenApp } from './model';
const client = new RedisClient(config.redisUrl, {
keyPrefix: 'tailchat:oidc:',
});
const grantable = new Set([
'AccessToken',
'AuthorizationCode',
'RefreshToken',
'DeviceCode',
'BackchannelAuthenticationRequest',
]);
const consumable = new Set([
'AuthorizationCode',
'RefreshToken',
'DeviceCode',
'BackchannelAuthenticationRequest',
]);
function grantKeyFor(id) {
return `grant:${id}`;
}
function userCodeKeyFor(userCode) {
return `userCode:${userCode}`;
}
function uidKeyFor(uid) {
return `uid:${uid}`;
}
/**
* Reference: https://github.com/panva/node-oidc-provider/blob/main/example/my_adapter.js
*/
export class TcOIDCAdapter implements Adapter {
constructor(public name: string) {}
async upsert(
id: string,
payload: AdapterPayload,
expiresIn: number
): Promise<undefined | void> {
const key = this.key(id);
const multi = client.multi();
if (consumable.has(this.name)) {
multi['hmset'](key, { payload: JSON.stringify(payload) });
} else {
multi['set'](key, JSON.stringify(payload));
}
if (expiresIn) {
multi.expire(key, expiresIn);
}
if (grantable.has(this.name) && payload.grantId) {
const grantKey = grantKeyFor(payload.grantId);
multi.rpush(grantKey, key);
// if you're seeing grant key lists growing out of acceptable proportions consider using LTRIM
// here to trim the list to an appropriate length
const ttl = await client.ttl(grantKey);
if (expiresIn > ttl) {
multi.expire(grantKey, expiresIn);
}
}
if (payload.userCode) {
const userCodeKey = userCodeKeyFor(payload.userCode);
multi.set(userCodeKey, id);
multi.expire(userCodeKey, expiresIn);
}
if (payload.uid) {
const uidKey = uidKeyFor(payload.uid);
multi.set(uidKey, id);
multi.expire(uidKey, expiresIn);
}
await multi.exec();
}
async find(id: string): Promise<AdapterPayload | undefined | void> {
if (this.name === 'Client') {
return this.findClient(id);
}
const data = consumable.has(this.name)
? await client.hgetall(this.key(id))
: await client.get(this.key(id));
if (_.isEmpty(data)) {
return undefined;
}
if (typeof data === 'string') {
return JSON.parse(data);
}
const { payload, ...rest } = data;
return {
...rest,
...JSON.parse(payload),
};
}
async findByUid(uid: string): Promise<AdapterPayload | undefined | void> {
const id = await client.get(uidKeyFor(uid));
return this.find(id);
}
async findByUserCode(
userCode: string
): Promise<AdapterPayload | undefined | void> {
const id = await client.get(userCodeKeyFor(userCode));
return this.find(id);
}
async destroy(id: string): Promise<undefined | void> {
const key = this.key(id);
await client.del(key);
}
async revokeByGrantId(grantId: string): Promise<undefined | void> {
// eslint-disable-line class-methods-use-this
const multi = client.multi();
const tokens = await client.lrange(grantKeyFor(grantId), 0, -1);
tokens.forEach((token) => multi.del(token));
multi.del(grantKeyFor(grantId));
await multi.exec();
}
async consume(id: string) {
await client.hset(this.key(id), 'consumed', Math.floor(Date.now() / 1000));
}
/**
* 查询客户端
*/
private async findClient(clientId: string): Promise<AdapterPayload | void> {
const app = await OpenApp.findOne({
appId: clientId,
}).exec();
if (!app) {
return;
}
if (!app.capability.includes('oauth')) {
return;
}
const clientPayload: AdapterPayload = {
client_id: app.appId,
client_secret: app.appSecret,
client_name: app.appName,
application_type: 'web',
grant_types: ['refresh_token', 'authorization_code'],
redirect_uris: [...(app.oauth?.redirectUrls ?? [])],
};
if (app.appIcon) {
clientPayload.logo_uri = app.appIcon;
}
return clientPayload;
}
key(id: string) {
return `${this.name}:${id}`;
}
}