mirror of
https://gitcode.com/gh_mirrors/hy/Hydro.git
synced 2026-10-17 09:36:08 +08:00
oauth-oidc: add generic oidc auth
This commit is contained in:
4 files changed
+148
No files matched your search
@@ -51,6 +51,7 @@
|
||||
"semver": "^7.7.4",
|
||||
"shorty.js": "^1.0.1",
|
||||
"superagent": "^10.3.0",
|
||||
"superagent-proxy": "^3.0.0",
|
||||
"tar": "6.2.1",
|
||||
"thirty-two": "^1.0.2",
|
||||
"ua-parser-js": "1.0.41"
|
||||
@@ -68,6 +69,7 @@
|
||||
"@types/notp": "^2.0.5",
|
||||
"@types/semver": "^7.7.1",
|
||||
"@types/superagent": "^8.1.9",
|
||||
"@types/superagent-proxy": "^3.0.4",
|
||||
"@types/ua-parser-js": "^0.7.39",
|
||||
"chokidar": "^5.0.0",
|
||||
"moment": "^2.30.1"
|
||||
|
||||
@@ -4,7 +4,11 @@ import _ from 'lodash';
|
||||
import { ObjectId as ObjectID } from 'mongodb';
|
||||
import Schema from 'schemastery';
|
||||
import superagent from 'superagent';
|
||||
import proxy from 'superagent-proxy';
|
||||
import { Context } from './context';
|
||||
|
||||
proxy(superagent);
|
||||
|
||||
export * from './interface';
|
||||
export * from './typeutils';
|
||||
export * from './utils';
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
import {
|
||||
Context, ForbiddenError, Handler, Schema, Service, superagent,
|
||||
SystemModel, TokenModel, UserFacingError, ValidationError,
|
||||
} from 'hydrooj';
|
||||
|
||||
const ProviderSchema = Schema.object({
|
||||
url: Schema.string().description('OIDC Discovery URL (.well-known/openid-configuration)').required(),
|
||||
clientId: Schema.string().description('Client ID').required(),
|
||||
clientSecret: Schema.string().description('Client Secret').role('secret').required(),
|
||||
name: Schema.string().description('Display name'),
|
||||
icon: Schema.string().description('SVG icon'),
|
||||
canRegister: Schema.boolean().default(true),
|
||||
scope: Schema.string().default('openid email profile'),
|
||||
proxy: Schema.string().description('Proxy URL (e.g. http://host:port, socks5://host:port)'),
|
||||
});
|
||||
|
||||
export const Config = Schema.object({
|
||||
providers: Schema.dict(ProviderSchema).default({}),
|
||||
});
|
||||
|
||||
interface OIDCConfig {
|
||||
authorization_endpoint: string;
|
||||
token_endpoint: string;
|
||||
userinfo_endpoint: string;
|
||||
jwks_uri?: string;
|
||||
}
|
||||
|
||||
const discoveryCache: Record<string, OIDCConfig> = {};
|
||||
|
||||
async function discover(url: string, proxy?: string): Promise<OIDCConfig> {
|
||||
if (discoveryCache[url]) return discoveryCache[url];
|
||||
let req = superagent.get(url).set('Accept', 'application/json');
|
||||
if (proxy) req = req.proxy(proxy);
|
||||
const res = await req;
|
||||
discoveryCache[url] = res.body;
|
||||
return res.body;
|
||||
}
|
||||
|
||||
function decodeJwtPayload(token: string) {
|
||||
const parts = token.split('.');
|
||||
if (parts.length !== 3) return null;
|
||||
try {
|
||||
return JSON.parse(Buffer.from(parts[1], 'base64url').toString());
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export default class LoginWithOIDCService extends Service {
|
||||
static inject = ['oauth'];
|
||||
static Config = Config;
|
||||
|
||||
constructor(ctx: Context, config: ReturnType<typeof Config>) {
|
||||
super(ctx, 'oauth.oidc');
|
||||
|
||||
for (const [key, provider] of Object.entries(config.providers)) {
|
||||
const displayName = provider.name || key;
|
||||
const agent = provider.proxy || undefined;
|
||||
ctx.oauth.provide(`oidc_${key}`, {
|
||||
text: `Login with ${displayName}`,
|
||||
name: displayName,
|
||||
icon: provider.icon || '',
|
||||
canRegister: provider.canRegister,
|
||||
get: async function get(this: Handler) {
|
||||
const oidcConfig = await discover(provider.url, agent);
|
||||
const [state] = await TokenModel.add(TokenModel.TYPE_OAUTH, 600, { redirect: this.request.referer });
|
||||
const url = SystemModel.get('server.url');
|
||||
const params = new URLSearchParams({
|
||||
client_id: provider.clientId,
|
||||
response_type: 'code',
|
||||
redirect_uri: `${url}oauth/oidc_${key}/callback`,
|
||||
scope: provider.scope,
|
||||
state,
|
||||
});
|
||||
this.response.redirect = `${oidcConfig.authorization_endpoint}?${params.toString()}`;
|
||||
},
|
||||
callback: async function callback(this: Handler, { state, code }) {
|
||||
const s = await TokenModel.get(state, TokenModel.TYPE_OAUTH);
|
||||
if (!s) throw new ValidationError('token');
|
||||
const oidcConfig = await discover(provider.url, agent);
|
||||
const url = SystemModel.get('server.url');
|
||||
let tokenReq = superagent.post(oidcConfig.token_endpoint)
|
||||
.type('form')
|
||||
.send({
|
||||
grant_type: 'authorization_code',
|
||||
client_id: provider.clientId,
|
||||
client_secret: provider.clientSecret,
|
||||
code,
|
||||
redirect_uri: `${url}oauth/oidc_${key}/callback`,
|
||||
});
|
||||
if (agent) tokenReq = tokenReq.proxy(agent);
|
||||
const tokenRes = await tokenReq;
|
||||
if (tokenRes.body.error) {
|
||||
throw new UserFacingError(
|
||||
tokenRes.body.error, tokenRes.body.error_description, tokenRes.body.error_uri,
|
||||
);
|
||||
}
|
||||
const accessToken = tokenRes.body.access_token;
|
||||
const idToken = tokenRes.body.id_token;
|
||||
|
||||
// Try to extract user info from id_token first, then fall back to userinfo endpoint
|
||||
let userInfo: any = null;
|
||||
if (idToken) {
|
||||
userInfo = decodeJwtPayload(idToken);
|
||||
}
|
||||
if (!userInfo?.email && oidcConfig.userinfo_endpoint) {
|
||||
let req = superagent.get(oidcConfig.userinfo_endpoint)
|
||||
.set('Authorization', `Bearer ${accessToken}`)
|
||||
.set('Accept', 'application/json');
|
||||
if (agent) req = req.proxy(agent);
|
||||
const res = await req;
|
||||
userInfo = { ...userInfo, ...res.body };
|
||||
}
|
||||
|
||||
await TokenModel.del(s._id, TokenModel.TYPE_OAUTH);
|
||||
if (!userInfo?.email) throw new ForbiddenError("Couldn't retrieve email from OIDC provider.");
|
||||
|
||||
return {
|
||||
_id: userInfo.sub || userInfo.email,
|
||||
email: userInfo.email,
|
||||
uname: [userInfo.preferred_username, userInfo.name].filter(Boolean),
|
||||
avatar: userInfo.picture || '',
|
||||
bio: '',
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
ctx.i18n.load('zh', {
|
||||
'Login with {0}': '使用 {0} 登录',
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"name": "@hydrooj/login-with-oidc",
|
||||
"version": "0.1.0",
|
||||
"main": "index.ts",
|
||||
"repository": "https://github.com/hydro-dev/Hydro",
|
||||
"author": "undefined <i@undefined.moe>",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"preferUnplugged": true
|
||||
}
|
||||
Reference in new issue
Block a user