Skip to content

Instantly share code, notes, and snippets.

@Aziz87
Last active September 2, 2024 12:24
Show Gist options
  • Save Aziz87/a805c78624ce6fda95c3ab5b43006684 to your computer and use it in GitHub Desktop.
Save Aziz87/a805c78624ce6fda95c3ab5b43006684 to your computer and use it in GitHub Desktop.
import { Body, Controller, Get, Headers, HttpException, HttpStatus, Post, Query, Req, Res, UnauthorizedException, UseGuards, UseInterceptors } from "@nestjs/common";
import { ApiBody, ApiExcludeEndpoint, ApiHeader, ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagger";
import { Response } from "express";
import { Tags } from "../../swagger/tags.enum";
import { ApiIntercepter } from "../../intercepters/api.intercepter";
import { AppleOAuthGuard } from "@app/shared/guards/apple-0auth.guard";
import { Public } from "@app/shared/guards";
import { AppleLoginDto } from "@app/shared/dto/user/apple-login.dto";
import { ExistingUserDTO, LoginResponse, NewUserDTO } from "@app/shared/dto";
import { UserEntity, UserRequest } from "@app/shared/interfaces/user";
import * as jwt from "jsonwebtoken";
import { JwksClient } from "jwks-rsa";
import { TransportService } from "@app/shared/modules/transport/transport.service";
import { EventPattern } from "@nestjs/microservices";
import { Topic } from "@app/shared/modules/transport/transport.topic";
import { CMD } from "@app/shared/modules/transport/transport.command";
import { ENV, RedisCacheService } from "@app/shared/modules";
import { AppleAuthCallbackDto } from "@app/shared/dto/user/apple-auth-callback.dto";
import { JwtService } from "@nestjs/jwt";
@UseInterceptors(ApiIntercepter)
@ApiTags(Tags.USER)
@Controller("user/apple")
export class APIAppleController {
private jwts = new JwtService();
constructor(
private readonly transport: TransportService,
private readonly redis: RedisCacheService,
private readonly env: ENV,
) {}
@EventPattern([Topic.USER + ".reply"])
onUserReply(payload: any) {
this.transport.resolvePromise(payload.uuid, payload.response);
}
private client = new JwksClient({
jwksUri: "https://appleid.apple.com/auth/keys",
});
private async getApplePublicKey(kid: string): Promise<string> {
return this.redis.cached(10000, `apple_public_key_${kid}`, async () => {
const key = await this.client.getSigningKey(kid);
return key.getPublicKey();
});
}
@Public()
@ApiOperation({
summary: "👨🏻‍💻 OAuth-Apple авторизация",
description: `Позволяет пользователю авторизоваться с помощью учетной записи Apple.
**Процесс работы:**
1. Пользователь нажимает кнопку авторизации Apple, и происходит редирект на страницу авторизации Apple.
2. Пользователь авторизуется с помощью учетной записи Apple, и происходит редирект на callback-URL Apple.
3. Если пользователь уже существует, то перекидывает на страницу /apple-login?token={token}&refreshToken={refreshToken}.
4. Используйте token для получения информации о пользователе.
`,
})
@ApiResponse({ status: 200 })
@Get("")
@UseGuards(AppleOAuthGuard)
async appleAuthRedirect() {}
@Post("callback")
@Public()
@ApiOperation({
summary: "👨🏻‍💻 OAuth-Apple",
description: `Обработка коллбэка после авторизации через Apple. Информация о пользователе Apple проверяется, а если пользователь авторизуется впервые, то создается новый аккаунт.
**Принцип работы:**
1. Информация о пользователе, полученная от Apple, проверяется.
2. Если пользователь авторизуется впервые, создается новый аккаунт.
3. Если пользователь уже существует, то перекидывает на страницу /apple-login?token={token}&refreshToken={refreshToken}.
4. Используйте token для получения информации о пользователе.
**Важное:**
- Если информация о пользователе Apple недействительна, то авторизация может не пройти.
`,
})
@ApiResponse({ status: 200 })
@ApiExcludeEndpoint()
// @UseGuards(AppleOAuthGuard)
async appleCallback(@Res() res: Response, @Body() dto: AppleAuthCallbackDto) {
console.log("onCallback", dto);
const auth = await this.authResponse({ identityToken: dto.id_token, firstName: dto.user?.name?.firstName, lastName: dto.user?.name?.lastName }, undefined);
let redirectUrl = "https://" + this.env.get("DOMAIN") + "/apple-login/";
redirectUrl += `?token=${auth.token}`;
redirectUrl += `&refreshToken=${auth.refreshToken}`;
res.redirect(redirectUrl);
}
@Post("login")
@Public()
@ApiOperation({
summary: "👨🏻‍💻 OAuth - Apple авторизация на мобильном устройстве",
description: `
**Процесс выполнения:**
1. Декодируется identityToken, полученный после успешной аутентификации Apple.
2. Если пользователь впервые логинится, создается новый аккаунт, возвращается такой же ответ что и при методе /login.
3. Если пользователь уже существует, информация о нем обновляется, возвращается такой же ответ что и при методе /login.
**Важное замечание:**
- Если токен аутентификации Apple недействителен, возникнет ошибка.
`,
})
@ApiBody({ type: AppleLoginDto })
@ApiResponse({ status: 201, type: LoginResponse })
@ApiHeader({ name: "authorization", required: false })
async appleLogin(@Req() req: UserRequest, @Body() dto: AppleLoginDto, @Headers("authorization") authorization: string) {
if (authorization) {
const authHeaderParts = (authorization as string).split(" ");
if (authHeaderParts.length === 2) {
const [, jwt] = authHeaderParts;
const data = await this.jwts.verifyAsync(jwt, { secret: this.env.get("JWT_SECRET") });
let { exp, timestamp, ...user } = data;
if (exp - timestamp < 60) throw new UnauthorizedException("Token expired");
return this.authResponse(dto, user.id);
}
} else {
return this.authResponse(dto, undefined);
}
}
private async authResponse(dto: AppleLoginDto, oldUserId: number): Promise<LoginResponse> {
const decodedIdToken = await this.verifyAppleIdToken(dto.identityToken);
console.log("decodedIdToken", decodedIdToken);
if (decodedIdToken.exp < Date.now() / 1000) throw new UnauthorizedException("Token expired");
if (!decodedIdToken.sub) throw new UnauthorizedException("Invalid apple token, sub not found");
if (oldUserId) {
const user:UserEntity = await this.transport.emitPromise(Topic.USER, CMD.USER_GET_BY_ID, { id: oldUserId });
const email = decodedIdToken.email || user?.email || decodedIdToken.sub + "@apple_wo_email.com";
if (!user) throw new UnauthorizedException("Old user not found");
const loginDto = new ExistingUserDTO();
loginDto.email = email;
loginDto.password = decodedIdToken.sub;
loginDto.appleId = decodedIdToken.sub;
loginDto.oldUserId = oldUserId;
return await this.transport.emitPromise(Topic.USER, CMD.USER_LOGIN, loginDto);
} else {
const email = decodedIdToken.email || decodedIdToken.sub + "@apple_wo_email.com";
const user: UserEntity = await this.transport.emitPromise(Topic.USER, CMD.USER_GET_BY_APPLE, { appleId: decodedIdToken.sub, email:decodedIdToken.email });
if (!user) {
const regDto = new NewUserDTO();
regDto.email = email;
regDto.password = decodedIdToken.sub;
regDto.firstName = dto.firstName || "";
regDto.lastName = dto.lastName || "";
regDto.emailVerified = true;
regDto.bdate = dto.bdate || null;
regDto.gender = dto.gender || null;
regDto.appleId = decodedIdToken.sub;
return await this.transport.emitPromise(Topic.USER, CMD.USER_REG, regDto);
} else {
const loginDto = new ExistingUserDTO();
loginDto.email = email;
loginDto.password = decodedIdToken.sub;
loginDto.appleId = decodedIdToken.sub;
return await this.transport.emitPromise(Topic.USER, CMD.USER_LOGIN, loginDto);
}
}
}
private async verifyAppleIdToken(idToken: string): Promise<any> {
const decodedHeader: any = jwt.decode(idToken, { complete: true });
if (!decodedHeader || !decodedHeader.header) throw new UnauthorizedException("Invalid apple token");
let audience = decodedHeader?.payload?.aud?.split(".").slice(0,3).join(".") === this.env.get("APPLE_AUTH_CLIENT_ID").split(".").slice(0,3).join(".") ? decodedHeader?.payload?.aud : this.env.get("APPLE_AUTH_CLIENT_ID")
const publicKey = await this.getApplePublicKey(decodedHeader.header.kid);
return new Promise((resolve, reject) => {
jwt.verify(idToken, publicKey, { algorithms: ["RS256"], issuer: "https://appleid.apple.com", audience }, (err, decoded) => {
if (err) {
console.log("err", err);
return reject(new UnauthorizedException());
}
resolve(decoded);
});
});
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment