mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2025-11-13 16:22:59 +10:00
Implement OpenID Connect Authentication Strategy (works with Keycloak, Authentik etc.)
This commit is contained in:
@ -35,6 +35,7 @@ import { GitHubGuard } from "./guards/github.guard";
|
||||
import { GoogleGuard } from "./guards/google.guard";
|
||||
import { JwtGuard } from "./guards/jwt.guard";
|
||||
import { LocalGuard } from "./guards/local.guard";
|
||||
import { OpenIDGuard } from "./guards/openid.guard";
|
||||
import { RefreshGuard } from "./guards/refresh.guard";
|
||||
import { TwoFactorGuard } from "./guards/two-factor.guard";
|
||||
import { getCookieOptions } from "./utils/cookie";
|
||||
@ -147,6 +148,23 @@ export class AuthController {
|
||||
return this.handleAuthenticationResponse(user, response, false, true);
|
||||
}
|
||||
|
||||
@ApiTags("OAuth", "OpenID")
|
||||
@Get("openid")
|
||||
@UseGuards(OpenIDGuard)
|
||||
openidLogin() {
|
||||
return;
|
||||
}
|
||||
|
||||
@ApiTags("OAuth", "OpenID")
|
||||
@Get("openid/callback")
|
||||
@UseGuards(OpenIDGuard)
|
||||
async openidCallback(
|
||||
@User() user: UserWithSecrets,
|
||||
@Res({ passthrough: true }) response: Response,
|
||||
) {
|
||||
return this.handleAuthenticationResponse(user, response, false, true);
|
||||
}
|
||||
|
||||
@Post("refresh")
|
||||
@UseGuards(RefreshGuard)
|
||||
async refresh(@User() user: UserWithSecrets, @Res({ passthrough: true }) response: Response) {
|
||||
|
||||
@ -14,6 +14,7 @@ import { GitHubStrategy } from "./strategy/github.strategy";
|
||||
import { GoogleStrategy } from "./strategy/google.strategy";
|
||||
import { JwtStrategy } from "./strategy/jwt.strategy";
|
||||
import { LocalStrategy } from "./strategy/local.strategy";
|
||||
import { OpenIDStrategy } from "./strategy/openid.strategy";
|
||||
import { RefreshStrategy } from "./strategy/refresh.strategy";
|
||||
import { TwoFactorStrategy } from "./strategy/two-factor.strategy";
|
||||
|
||||
@ -63,6 +64,35 @@ export class AuthModule {
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
provide: OpenIDStrategy,
|
||||
inject: [ConfigService, UserService],
|
||||
useFactory: (configService: ConfigService<Config>, userService: UserService) => {
|
||||
try {
|
||||
const authorizationURL = configService.getOrThrow("OPENID_AUTHORIZATION_URL");
|
||||
const callbackURL = configService.getOrThrow("OPENID_CALLBACK_URL");
|
||||
const clientID = configService.getOrThrow("OPENID_CLIENT_ID");
|
||||
const clientSecret = configService.getOrThrow("OPENID_CLIENT_SECRET");
|
||||
const issuer = configService.getOrThrow("OPENID_ISSUER");
|
||||
const tokenURL = configService.getOrThrow("OPENID_TOKEN_URL");
|
||||
const userInfoURL = configService.getOrThrow("OPENID_USER_INFO_URL");
|
||||
|
||||
return new OpenIDStrategy(
|
||||
authorizationURL,
|
||||
callbackURL,
|
||||
clientID,
|
||||
clientSecret,
|
||||
issuer,
|
||||
tokenURL,
|
||||
userInfoURL,
|
||||
userService,
|
||||
);
|
||||
} catch {
|
||||
return new DummyStrategy();
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
exports: [AuthService],
|
||||
};
|
||||
|
||||
@ -199,6 +199,14 @@ export class AuthService {
|
||||
providers.push("google");
|
||||
}
|
||||
|
||||
if (
|
||||
this.configService.get("OPENID_CLIENT_ID") &&
|
||||
this.configService.get("OPENID_CLIENT_SECRET") &&
|
||||
this.configService.get("OPENID_CALLBACK_URL")
|
||||
) {
|
||||
providers.push("openid");
|
||||
}
|
||||
|
||||
return providers;
|
||||
}
|
||||
|
||||
|
||||
5
apps/server/src/auth/guards/openid.guard.ts
Normal file
5
apps/server/src/auth/guards/openid.guard.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { AuthGuard } from "@nestjs/passport";
|
||||
|
||||
@Injectable()
|
||||
export class OpenIDGuard extends AuthGuard("openid") {}
|
||||
74
apps/server/src/auth/strategy/openid.strategy.ts
Normal file
74
apps/server/src/auth/strategy/openid.strategy.ts
Normal file
@ -0,0 +1,74 @@
|
||||
import { BadRequestException, Injectable } from "@nestjs/common";
|
||||
import { PassportStrategy } from "@nestjs/passport";
|
||||
import { User } from "@prisma/client";
|
||||
import { ErrorMessage, processUsername } from "@reactive-resume/utils";
|
||||
import { Profile, Strategy, StrategyOptions } from "passport-openidconnect";
|
||||
|
||||
import { UserService } from "@/server/user/user.service";
|
||||
|
||||
@Injectable()
|
||||
export class OpenIDStrategy extends PassportStrategy(Strategy, "openid") {
|
||||
constructor(
|
||||
readonly authorizationURL: string,
|
||||
readonly callbackURL: string,
|
||||
readonly clientID: string,
|
||||
readonly clientSecret: string,
|
||||
readonly issuer: string,
|
||||
readonly tokenURL: string,
|
||||
readonly userInfoURL: string,
|
||||
private readonly userService: UserService,
|
||||
) {
|
||||
super({
|
||||
authorizationURL,
|
||||
callbackURL,
|
||||
clientID,
|
||||
clientSecret,
|
||||
issuer,
|
||||
tokenURL,
|
||||
userInfoURL,
|
||||
scope: "openid email profile",
|
||||
} as StrategyOptions);
|
||||
}
|
||||
|
||||
async validate(
|
||||
issuer: unknown,
|
||||
profile: Profile,
|
||||
done: (err?: string | Error | null, user?: Express.User, info?: unknown) => void,
|
||||
) {
|
||||
const { displayName, emails, photos, username } = profile;
|
||||
|
||||
const email = emails?.[0].value ?? `${username}@github.com`;
|
||||
const picture = photos?.[0].value;
|
||||
|
||||
let user: User | null = null;
|
||||
|
||||
if (!email) throw new BadRequestException(ErrorMessage.InvalidCredentials);
|
||||
|
||||
try {
|
||||
const user =
|
||||
(await this.userService.findOneByIdentifier(email)) ??
|
||||
(username && (await this.userService.findOneByIdentifier(username)));
|
||||
|
||||
if (!user) throw new Error(ErrorMessage.InvalidCredentials);
|
||||
|
||||
done(null, user);
|
||||
} catch {
|
||||
try {
|
||||
user = await this.userService.create({
|
||||
email,
|
||||
picture,
|
||||
locale: "en-US",
|
||||
name: displayName,
|
||||
provider: "openid",
|
||||
emailVerified: true, // auto-verify emails
|
||||
username: processUsername(username ?? email.split("@")[0]),
|
||||
secrets: { create: {} },
|
||||
});
|
||||
|
||||
done(null, user);
|
||||
} catch {
|
||||
throw new BadRequestException(ErrorMessage.UserAlreadyExists);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -63,15 +63,24 @@ export const configSchema = z.object({
|
||||
.default("false")
|
||||
.transform((s) => s !== "false" && s !== "0"),
|
||||
|
||||
// GitHub (OAuth)
|
||||
// GitHub (OAuth, Optional)
|
||||
GITHUB_CLIENT_ID: z.string().optional(),
|
||||
GITHUB_CLIENT_SECRET: z.string().optional(),
|
||||
GITHUB_CALLBACK_URL: z.string().url().optional(),
|
||||
|
||||
// Google (OAuth)
|
||||
// Google (OAuth, Optional)
|
||||
GOOGLE_CLIENT_ID: z.string().optional(),
|
||||
GOOGLE_CLIENT_SECRET: z.string().optional(),
|
||||
GOOGLE_CALLBACK_URL: z.string().url().optional(),
|
||||
|
||||
// OpenID (Optional)
|
||||
OPENID_AUTHORIZATION_URL: z.string().url().optional(),
|
||||
OPENID_CALLBACK_URL: z.string().url().optional(),
|
||||
OPENID_CLIENT_ID: z.string().optional(),
|
||||
OPENID_CLIENT_SECRET: z.string().optional(),
|
||||
OPENID_ISSUER: z.string().optional(),
|
||||
OPENID_TOKEN_URL: z.string().url().optional(),
|
||||
OPENID_USER_INFO_URL: z.string().url().optional(),
|
||||
});
|
||||
|
||||
export type Config = z.infer<typeof configSchema>;
|
||||
|
||||
@ -4,6 +4,7 @@ import { NestFactory } from "@nestjs/core";
|
||||
import { NestExpressApplication } from "@nestjs/platform-express";
|
||||
import { DocumentBuilder, SwaggerModule } from "@nestjs/swagger";
|
||||
import cookieParser from "cookie-parser";
|
||||
import session from "express-session";
|
||||
import helmet from "helmet";
|
||||
import { patchNestJsSwagger } from "nestjs-zod";
|
||||
|
||||
@ -21,6 +22,15 @@ async function bootstrap() {
|
||||
// Cookie Parser
|
||||
app.use(cookieParser());
|
||||
|
||||
// Session
|
||||
app.use(
|
||||
session({
|
||||
resave: false,
|
||||
saveUninitialized: false,
|
||||
secret: configService.getOrThrow("ACCESS_TOKEN_SECRET"),
|
||||
}),
|
||||
);
|
||||
|
||||
// CORS
|
||||
app.enableCors({
|
||||
credentials: true,
|
||||
|
||||
Reference in New Issue
Block a user