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.
Hydro/packages/login-with-google/index.ts

85 lines
2.9 KiB
TypeScript

import {
Context, Handler, superagent, SystemModel, TokenModel, UserFacingError,
} from 'hydrooj';
declare module 'hydrooj' {
interface SystemKeys {
'login-with-google.id': string,
'login-with-google.secret': string,
}
}
async function get(this: Handler) {
const [appid, url, [state]] = await Promise.all([
SystemModel.get('login-with-google.id'),
SystemModel.get('server.url'),
TokenModel.add(TokenModel.TYPE_OAUTH, 600, { redirect: this.request.referer }),
]);
// eslint-disable-next-line max-len
this.response.redirect = `https://accounts.google.com/o/oauth2/v2/auth?client_id=${appid}&response_type=code&redirect_uri=${url}oauth/google/callback&scope=https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile&state=${state}`;
}
function unescapedString(escapedString: string) {
escapedString += new Array(5 - (escapedString.length % 4)).join('=');
return escapedString.replace(/-/g, '+').replace(/_/g, '/');
}
function decodeJWT(idToken: string) {
const token = idToken.split('.');
if (token.length !== 3) throw new Error('Invalid idToken');
try {
const headerSegment = JSON.parse(Buffer.from(token[0], 'base64').toString('utf8'));
const payloadSegment = JSON.parse(Buffer.from(token[1], 'base64').toString('utf8'));
const signature = unescapedString(token[2]);
return {
dataToSign: [token[0], token[1]].join('.'),
header: headerSegment,
payload: payloadSegment,
signature,
};
} catch (e) {
throw new Error('Invalid payload');
}
}
async function callback(this: Handler, {
state, code, error,
}) {
if (error) throw new UserFacingError(error);
const [[appid, secret, url], s] = await Promise.all([
SystemModel.getMany([
'login-with-google.id', 'login-with-google.secret', 'server.url',
]),
TokenModel.get(state, TokenModel.TYPE_OAUTH),
]);
const res = await superagent.post('https://oauth2.googleapis.com/token')
.send({
client_id: appid,
client_secret: secret,
code,
grant_type: 'authorization_code',
redirect_uri: `${url}oauth/google/callback`,
});
const payload = decodeJWT(res.body.id_token).payload;
await TokenModel.del(state, TokenModel.TYPE_OAUTH);
this.response.redirect = s.redirect;
return {
// TODO use openid
_id: payload.email,
email: payload.email,
uname: [payload.given_name, payload.name, payload.family_name],
viewLang: payload.locale.replace('-', '_'),
};
}
export function apply(ctx: Context) {
ctx.provideModule('oauth', 'google', {
text: 'Login with Google',
callback,
get,
});
ctx.i18n.load('zh', {
'Login With Google': '使用 Google 登录',
});
}