Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .env.example
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
c# change to production before deploying
# change to production before deploying
NODE_ENV='development'
PORT=5000
APP_ID='00000000-0000-0000-0000-000000000000'
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# Nest OAuth: Adding External Providers
# Nest OAuth: Adding Mobile Apps Support

## Intro

This is the source code for the
tutorial [Nest Authentication with OAuth2.0](https://dev.to/tugascript/nestjs-authentication-with-oauth20-adding-external-providers-2kj).
This is the 5<sup>th</sup> and last part on a 5 part series, where we will build a production level NestJS OAuth2
tutorial [Nest Authentication with OAuth2.0](https://dev.to/tugascript/nestjs-authentication-with-oauth20-adding-mobile-apps-support-13nl).
This is the 6<sup>th</sup> and extra part on a 5 part series, where we will build a production level NestJS OAuth2
service.

### Contents
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
"class-transformer": "0.5.1",
"class-validator": "0.14.1",
"dayjs": "1.11.12",
"fastify": "4.28.0",
"fastify": "4.28.1",
"handlebars": "4.7.8",
"ioredis": "5.4.1",
"joi": "17.13.3",
Expand Down Expand Up @@ -82,6 +82,7 @@
"eslint-plugin-header": "3.1.1",
"eslint-plugin-prettier": "5.2.1",
"jest": "29.7.0",
"nock": "13.5.5",
"prettier": "3.3.3",
"source-map-support": "0.5.21",
"supertest": "7.0.0",
Expand Down
18 changes: 14 additions & 4 deletions src/auth/auth.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ import { Public } from './decorators/public.decorator';
import { ChangePasswordDto } from './dtos/change-password.dto';
import { ConfirmEmailDto } from './dtos/confirm-email.dto';
import { EmailDto } from './dtos/email.dto';
import { RefreshAccessDto } from './dtos/refresh-access.dto';
import { ResetPasswordDto } from './dtos/reset-password.dto';
import { SignInDto } from './dtos/sign-in.dto';
import { SignUpDto } from './dtos/sign-up.dto';
Expand Down Expand Up @@ -136,8 +137,9 @@ export class AuthController {
public async refreshAccess(
@Req() req: FastifyRequest,
@Res() res: FastifyReply,
@Body() refreshAccessDto?: RefreshAccessDto,
): Promise<void> {
const token = this.refreshTokenFromReq(req);
const token = this.refreshTokenFromReq(req, refreshAccessDto);
const result = await this.authService.refreshTokenAccess(
token,
req.headers.origin,
Expand All @@ -161,8 +163,9 @@ export class AuthController {
public async logout(
@Req() req: FastifyRequest,
@Res() res: FastifyReply,
@Body() refreshAccessDto?: RefreshAccessDto,
): Promise<void> {
const token = this.refreshTokenFromReq(req);
const token = this.refreshTokenFromReq(req, refreshAccessDto);
const message = await this.authService.logout(token);
res
.clearCookie(this.cookieName, { path: this.cookiePath })
Expand All @@ -189,7 +192,7 @@ export class AuthController {
@Body() confirmEmailDto: ConfirmEmailDto,
@Res() res: FastifyReply,
): Promise<void> {
const result = await this.authService.confirmEmail(confirmEmailDto);
const result = await this.authService.confirmEmail(confirmEmailDto, origin);
this.saveRefreshCookie(res, result.refreshToken)
.status(HttpStatus.OK)
.send(AuthResponseMapper.map(result));
Expand Down Expand Up @@ -279,10 +282,17 @@ export class AuthController {
return OAuthProvidersResponseMapper.map(providers);
}

private refreshTokenFromReq(req: FastifyRequest): string {
private refreshTokenFromReq(
req: FastifyRequest,
dto?: RefreshAccessDto,
): string {
const token: string | undefined = req.cookies[this.cookieName];

if (isUndefined(token) || isNull(token)) {
if (!isUndefined(dto?.refreshToken)) {
return dto.refreshToken;
}

throw new UnauthorizedException();
}

Expand Down
28 changes: 24 additions & 4 deletions src/auth/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,12 @@ export class AuthService {
const user = await this.usersService.confirmEmail(id, version);
const [accessToken, refreshToken] =
await this.jwtService.generateAuthTokens(user, domain);
return { user, accessToken, refreshToken };
return {
user,
accessToken,
refreshToken,
expiresIn: this.jwtService.accessTime,
};
}

public async signIn(dto: SignInDto, domain?: string): Promise<IAuthResult> {
Expand All @@ -112,7 +117,12 @@ export class AuthService {

const [accessToken, refreshToken] =
await this.jwtService.generateAuthTokens(user, domain);
return { user, accessToken, refreshToken };
return {
user,
accessToken,
refreshToken,
expiresIn: this.jwtService.accessTime,
};
}

public async refreshTokenAccess(
Expand All @@ -128,7 +138,12 @@ export class AuthService {
const user = await this.usersService.findOneByCredentials(id, version);
const [accessToken, newRefreshToken] =
await this.jwtService.generateAuthTokens(user, domain, tokenId);
return { user, accessToken, refreshToken: newRefreshToken };
return {
user,
accessToken,
refreshToken: newRefreshToken,
expiresIn: this.jwtService.accessTime,
};
}

public async logout(refreshToken: string): Promise<IMessage> {
Expand Down Expand Up @@ -184,7 +199,12 @@ export class AuthService {
);
const [accessToken, refreshToken] =
await this.jwtService.generateAuthTokens(user, domain);
return { user, accessToken, refreshToken };
return {
user,
accessToken,
refreshToken,
expiresIn: this.jwtService.accessTime,
};
}

private async checkLastPassword(
Expand Down
32 changes: 32 additions & 0 deletions src/auth/dtos/refresh-access.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
Copyright (C) 2024 Afonso Barracha

Nest OAuth is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

Nest OAuth is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public License
along with Nest OAuth. If not, see <https://www.gnu.org/licenses/>.
*/

import { ApiProperty } from '@nestjs/swagger';
import { IsJWT, IsOptional, IsString } from 'class-validator';

export abstract class RefreshAccessDto {
@ApiProperty({
description: 'The JWT token sent to the user email',
example:
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c',
type: String,
})
@IsOptional()
@IsString()
@IsJWT()
public refreshToken?: string;
}
3 changes: 3 additions & 0 deletions src/auth/interfaces/auth-response.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,7 @@ import { IAuthResponseUser } from './auth-response-user.interface';
export interface IAuthResponse {
user: IAuthResponseUser;
accessToken: string;
refreshToken: string;
tokenType: string;
expiresIn: number;
}
1 change: 1 addition & 0 deletions src/auth/interfaces/auth-result.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,5 @@ export interface IAuthResult {
user: IUser;
accessToken: string;
refreshToken: string;
expiresIn: number;
}
25 changes: 25 additions & 0 deletions src/auth/mappers/auth-response.mapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,28 @@ export class AuthResponseMapper implements IAuthResponse {
})
public readonly accessToken: string;

@ApiProperty({
description: 'Refresh token',
example:
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c',
type: String,
})
public readonly refreshToken: string;

@ApiProperty({
description: 'Token type',
example: 'Bearer',
type: String,
})
public readonly tokenType: string;

@ApiProperty({
description: 'Expiration period in seconds',
example: 3600,
type: Number,
})
public readonly expiresIn: number;

constructor(values: IAuthResponse) {
Object.assign(this, values);
}
Expand All @@ -43,6 +65,9 @@ export class AuthResponseMapper implements IAuthResponse {
return new AuthResponseMapper({
user: AuthResponseUserMapper.map(result.user),
accessToken: result.accessToken,
refreshToken: result.refreshToken,
tokenType: 'Bearer',
expiresIn: result.expiresIn,
});
}
}
15 changes: 15 additions & 0 deletions src/common/common.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
Logger,
LoggerService,
NotFoundException,
UnauthorizedException,
} from '@nestjs/common';
import { validate } from 'class-validator';
import slugify from 'slugify';
Expand Down Expand Up @@ -131,6 +132,20 @@ export class CommonService {
}
}

/**
* Throw Unauthorized
*
* Function to abstract throwing unauthorized exceptionm
*/
public async throwUnauthorizedError<T>(promise: Promise<T>): Promise<T> {
try {
return await promise;
} catch (error) {
this.loggerService.error(error);
throw new UnauthorizedException();
}
}

/**
* Format Name
*
Expand Down
4 changes: 4 additions & 0 deletions src/jwt/jwt.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ export class JwtService {
this.domain = this.configService.get<string>('domain');
}

public get accessTime(): number {
return this.jwtConfig.access.time;
}

private static async generateTokenAsync(
payload: IAccessPayload | IEmailPayload | IRefreshPayload,
secret: string,
Expand Down
41 changes: 41 additions & 0 deletions src/oauth2/dtos/token.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
Copyright (C) 2024 Afonso Barracha

Nest OAuth is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

Nest OAuth is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public License
along with Nest OAuth. If not, see <https://www.gnu.org/licenses/>.
*/

import { ApiProperty } from '@nestjs/swagger';
import { IsString, IsUrl, Length } from 'class-validator';

export abstract class TokenDto {
@ApiProperty({
description: 'The Code to exchange for a token',
example: '5WA0R4DVyWThKFnc73z7nT',
minLength: 1,
maxLength: 22,
type: String,
})
@IsString()
@Length(1, 22)
public code: string;

@ApiProperty({
description: 'Redirect URI that was used to get the token',
example: 'https://example.com/auth/callback',
type: String,
})
@IsString()
@IsUrl()
public redirectUri: string;
}
2 changes: 2 additions & 0 deletions src/oauth2/guards/oauth-flag.guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import {
CanActivate,
ExecutionContext,
Injectable,
mixin,
NotFoundException,
Type,
Expand All @@ -31,6 +32,7 @@ import { IClient } from '../interfaces/client.interface';
export const OAuthFlagGuard = (
provider: OAuthProvidersEnum,
): Type<CanActivate> => {
@Injectable()
class OAuthFlagGuardClass implements CanActivate {
constructor(private readonly configService: ConfigService) {}

Expand Down
22 changes: 22 additions & 0 deletions src/oauth2/interfaces/callback-result.interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/*
Copyright (C) 2024 Afonso Barracha

Nest OAuth is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

Nest OAuth is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public License
along with Nest OAuth. If not, see <https://www.gnu.org/licenses/>.
*/

export interface ICallbackResult {
readonly code: string;
readonly accessToken: string;
readonly expiresIn: number;
}
Loading