refactor(server): auth (#5895)

Remove `next-auth` and implement our own Authorization/Authentication system from scratch.

## Server

- [x] tokens
  - [x] function
  - [x] encryption

- [x] AuthController
  - [x] /api/auth/sign-in
  - [x] /api/auth/sign-out
  - [x] /api/auth/session
  - [x] /api/auth/session (WE SUPPORT MULTI-ACCOUNT!)

- [x] OAuthPlugin
  - [x] OAuthController
  - [x] /oauth/login
  - [x] /oauth/callback
  - [x] Providers
    - [x] Google
    - [x] GitHub

## Client

- [x] useSession
- [x] cloudSignIn
- [x] cloudSignOut

## NOTE:

Tests will be adding in the future
This commit is contained in:
liuyi
2024-03-12 10:00:09 +00:00
parent af49e8cc41
commit fb3a0e7b8f
148 changed files with 3407 additions and 2851 deletions
@@ -0,0 +1,21 @@
import { OAuthProviderName } from '../types';
export interface OAuthAccount {
id: string;
email: string;
avatarUrl?: string;
}
export interface Tokens {
accessToken: string;
scope?: string;
refreshToken?: string;
expiresAt?: Date;
}
export abstract class OAuthProvider {
abstract provider: OAuthProviderName;
abstract getAuthUrl(state?: string): string;
abstract getToken(code: string): Promise<Tokens>;
abstract getUser(token: string): Promise<OAuthAccount>;
}
@@ -0,0 +1,113 @@
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { Config, URLHelper } from '../../../fundamentals';
import { AutoRegisteredOAuthProvider } from '../register';
import { OAuthProviderName } from '../types';
interface AuthTokenResponse {
access_token: string;
scope: string;
token_type: string;
}
export interface UserInfo {
login: string;
email: string;
avatar_url: string;
name: string;
}
@Injectable()
export class GithubOAuthProvider extends AutoRegisteredOAuthProvider {
provider = OAuthProviderName.GitHub;
constructor(
protected readonly AFFiNEConfig: Config,
private readonly url: URLHelper
) {
super();
}
getAuthUrl(state: string) {
return `https://github.com/login/oauth/authorize?${this.url.stringify({
client_id: this.config.clientId,
redirect_uri: this.url.link('/oauth/callback'),
scope: 'user',
...this.config.args,
state,
})}`;
}
async getToken(code: string) {
try {
const response = await fetch(
'https://github.com/login/oauth/access_token',
{
method: 'POST',
body: this.url.stringify({
code,
client_id: this.config.clientId,
client_secret: this.config.clientSecret,
redirect_uri: this.url.link('/oauth/callback'),
}),
headers: {
Accept: 'application/json',
'Content-Type': 'application/x-www-form-urlencoded',
},
}
);
if (response.ok) {
const ghToken = (await response.json()) as AuthTokenResponse;
return {
accessToken: ghToken.access_token,
scope: ghToken.scope,
};
} else {
throw new Error(
`Server responded with non-success code ${
response.status
}, ${JSON.stringify(await response.json())}`
);
}
} catch (e) {
throw new HttpException(
`Failed to get access_token, err: ${(e as Error).message}`,
HttpStatus.BAD_REQUEST
);
}
}
async getUser(token: string) {
try {
const response = await fetch('https://api.github.com/user', {
method: 'GET',
headers: {
Authorization: `Bearer ${token}`,
},
});
if (response.ok) {
const user = (await response.json()) as UserInfo;
return {
id: user.login,
avatarUrl: user.avatar_url,
email: user.email,
};
} else {
throw new Error(
`Server responded with non-success code ${
response.status
} ${await response.text()}`
);
}
} catch (e) {
throw new HttpException(
`Failed to get user information, err: ${(e as Error).stack}`,
HttpStatus.BAD_REQUEST
);
}
}
}
@@ -0,0 +1,121 @@
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { Config, URLHelper } from '../../../fundamentals';
import { AutoRegisteredOAuthProvider } from '../register';
import { OAuthProviderName } from '../types';
interface GoogleOAuthTokenResponse {
access_token: string;
expires_in: number;
refresh_token: string;
scope: string;
token_type: string;
}
export interface UserInfo {
id: string;
email: string;
picture: string;
name: string;
}
@Injectable()
export class GoogleOAuthProvider extends AutoRegisteredOAuthProvider {
override provider = OAuthProviderName.Google;
constructor(
protected readonly AFFiNEConfig: Config,
private readonly url: URLHelper
) {
super();
}
getAuthUrl(state: string) {
return `https://accounts.google.com/o/oauth2/v2/auth?${this.url.stringify({
client_id: this.config.clientId,
redirect_uri: this.url.link('/oauth/callback'),
response_type: 'code',
scope: 'openid email profile',
promot: 'select_account',
access_type: 'offline',
...this.config.args,
state,
})}`;
}
async getToken(code: string) {
try {
const response = await fetch('https://oauth2.googleapis.com/token', {
method: 'POST',
body: this.url.stringify({
code,
client_id: this.config.clientId,
client_secret: this.config.clientSecret,
redirect_uri: this.url.link('/oauth/callback'),
grant_type: 'authorization_code',
}),
headers: {
Accept: 'application/json',
'Content-Type': 'application/x-www-form-urlencoded',
},
});
if (response.ok) {
const ghToken = (await response.json()) as GoogleOAuthTokenResponse;
return {
accessToken: ghToken.access_token,
refreshToken: ghToken.refresh_token,
expiresAt: new Date(Date.now() + ghToken.expires_in * 1000),
scope: ghToken.scope,
};
} else {
throw new Error(
`Server responded with non-success code ${
response.status
}, ${JSON.stringify(await response.json())}`
);
}
} catch (e) {
throw new HttpException(
`Failed to get access_token, err: ${(e as Error).message}`,
HttpStatus.BAD_REQUEST
);
}
}
async getUser(token: string) {
try {
const response = await fetch(
'https://www.googleapis.com/oauth2/v2/userinfo',
{
method: 'GET',
headers: {
Authorization: `Bearer ${token}`,
},
}
);
if (response.ok) {
const user = (await response.json()) as UserInfo;
return {
id: user.id,
avatarUrl: user.picture,
email: user.email,
};
} else {
throw new Error(
`Server responded with non-success code ${
response.status
} ${await response.text()}`
);
}
} catch (e) {
throw new HttpException(
`Failed to get user information, err: ${(e as Error).stack}`,
HttpStatus.BAD_REQUEST
);
}
}
}
@@ -0,0 +1,4 @@
import { GithubOAuthProvider } from './github';
import { GoogleOAuthProvider } from './google';
export const OAuthProviders = [GoogleOAuthProvider, GithubOAuthProvider];