Skip to content

Commit 23ecc26

Browse files
authored
Merge pull request #79 from tugascript/mobile-external
feat(oauth2): add token endpoint
2 parents 7c1fc04 + a0face7 commit 23ecc26

20 files changed

+843
-93
lines changed

.env.example

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
c# change to production before deploying
1+
# change to production before deploying
22
NODE_ENV='development'
33
PORT=5000
44
APP_ID='00000000-0000-0000-0000-000000000000'

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
# Nest OAuth: Adding External Providers
1+
# Nest OAuth: Adding Mobile Apps Support
22

33
## Intro
44

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

1010
### Contents

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@
4646
"class-transformer": "0.5.1",
4747
"class-validator": "0.14.1",
4848
"dayjs": "1.11.12",
49-
"fastify": "4.28.0",
49+
"fastify": "4.28.1",
5050
"handlebars": "4.7.8",
5151
"ioredis": "5.4.1",
5252
"joi": "17.13.3",
@@ -82,6 +82,7 @@
8282
"eslint-plugin-header": "3.1.1",
8383
"eslint-plugin-prettier": "5.2.1",
8484
"jest": "29.7.0",
85+
"nock": "13.5.5",
8586
"prettier": "3.3.3",
8687
"source-map-support": "0.5.21",
8788
"supertest": "7.0.0",

src/auth/auth.controller.ts

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ import { Public } from './decorators/public.decorator';
4949
import { ChangePasswordDto } from './dtos/change-password.dto';
5050
import { ConfirmEmailDto } from './dtos/confirm-email.dto';
5151
import { EmailDto } from './dtos/email.dto';
52+
import { RefreshAccessDto } from './dtos/refresh-access.dto';
5253
import { ResetPasswordDto } from './dtos/reset-password.dto';
5354
import { SignInDto } from './dtos/sign-in.dto';
5455
import { SignUpDto } from './dtos/sign-up.dto';
@@ -136,8 +137,9 @@ export class AuthController {
136137
public async refreshAccess(
137138
@Req() req: FastifyRequest,
138139
@Res() res: FastifyReply,
140+
@Body() refreshAccessDto?: RefreshAccessDto,
139141
): Promise<void> {
140-
const token = this.refreshTokenFromReq(req);
142+
const token = this.refreshTokenFromReq(req, refreshAccessDto);
141143
const result = await this.authService.refreshTokenAccess(
142144
token,
143145
req.headers.origin,
@@ -161,8 +163,9 @@ export class AuthController {
161163
public async logout(
162164
@Req() req: FastifyRequest,
163165
@Res() res: FastifyReply,
166+
@Body() refreshAccessDto?: RefreshAccessDto,
164167
): Promise<void> {
165-
const token = this.refreshTokenFromReq(req);
168+
const token = this.refreshTokenFromReq(req, refreshAccessDto);
166169
const message = await this.authService.logout(token);
167170
res
168171
.clearCookie(this.cookieName, { path: this.cookiePath })
@@ -189,7 +192,7 @@ export class AuthController {
189192
@Body() confirmEmailDto: ConfirmEmailDto,
190193
@Res() res: FastifyReply,
191194
): Promise<void> {
192-
const result = await this.authService.confirmEmail(confirmEmailDto);
195+
const result = await this.authService.confirmEmail(confirmEmailDto, origin);
193196
this.saveRefreshCookie(res, result.refreshToken)
194197
.status(HttpStatus.OK)
195198
.send(AuthResponseMapper.map(result));
@@ -279,10 +282,17 @@ export class AuthController {
279282
return OAuthProvidersResponseMapper.map(providers);
280283
}
281284

282-
private refreshTokenFromReq(req: FastifyRequest): string {
285+
private refreshTokenFromReq(
286+
req: FastifyRequest,
287+
dto?: RefreshAccessDto,
288+
): string {
283289
const token: string | undefined = req.cookies[this.cookieName];
284290

285291
if (isUndefined(token) || isNull(token)) {
292+
if (!isUndefined(dto?.refreshToken)) {
293+
return dto.refreshToken;
294+
}
295+
286296
throw new UnauthorizedException();
287297
}
288298

src/auth/auth.service.ts

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,12 @@ export class AuthService {
8888
const user = await this.usersService.confirmEmail(id, version);
8989
const [accessToken, refreshToken] =
9090
await this.jwtService.generateAuthTokens(user, domain);
91-
return { user, accessToken, refreshToken };
91+
return {
92+
user,
93+
accessToken,
94+
refreshToken,
95+
expiresIn: this.jwtService.accessTime,
96+
};
9297
}
9398

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

113118
const [accessToken, refreshToken] =
114119
await this.jwtService.generateAuthTokens(user, domain);
115-
return { user, accessToken, refreshToken };
120+
return {
121+
user,
122+
accessToken,
123+
refreshToken,
124+
expiresIn: this.jwtService.accessTime,
125+
};
116126
}
117127

118128
public async refreshTokenAccess(
@@ -128,7 +138,12 @@ export class AuthService {
128138
const user = await this.usersService.findOneByCredentials(id, version);
129139
const [accessToken, newRefreshToken] =
130140
await this.jwtService.generateAuthTokens(user, domain, tokenId);
131-
return { user, accessToken, refreshToken: newRefreshToken };
141+
return {
142+
user,
143+
accessToken,
144+
refreshToken: newRefreshToken,
145+
expiresIn: this.jwtService.accessTime,
146+
};
132147
}
133148

134149
public async logout(refreshToken: string): Promise<IMessage> {
@@ -184,7 +199,12 @@ export class AuthService {
184199
);
185200
const [accessToken, refreshToken] =
186201
await this.jwtService.generateAuthTokens(user, domain);
187-
return { user, accessToken, refreshToken };
202+
return {
203+
user,
204+
accessToken,
205+
refreshToken,
206+
expiresIn: this.jwtService.accessTime,
207+
};
188208
}
189209

190210
private async checkLastPassword(
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/*
2+
Copyright (C) 2024 Afonso Barracha
3+
4+
Nest OAuth is free software: you can redistribute it and/or modify
5+
it under the terms of the GNU Lesser General Public License as published by
6+
the Free Software Foundation, either version 3 of the License, or
7+
(at your option) any later version.
8+
9+
Nest OAuth is distributed in the hope that it will be useful,
10+
but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
GNU Lesser General Public License for more details.
13+
14+
You should have received a copy of the GNU Lesser General Public License
15+
along with Nest OAuth. If not, see <https://www.gnu.org/licenses/>.
16+
*/
17+
18+
import { ApiProperty } from '@nestjs/swagger';
19+
import { IsJWT, IsOptional, IsString } from 'class-validator';
20+
21+
export abstract class RefreshAccessDto {
22+
@ApiProperty({
23+
description: 'The JWT token sent to the user email',
24+
example:
25+
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c',
26+
type: String,
27+
})
28+
@IsOptional()
29+
@IsString()
30+
@IsJWT()
31+
public refreshToken?: string;
32+
}

src/auth/interfaces/auth-response.interface.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,7 @@ import { IAuthResponseUser } from './auth-response-user.interface';
2020
export interface IAuthResponse {
2121
user: IAuthResponseUser;
2222
accessToken: string;
23+
refreshToken: string;
24+
tokenType: string;
25+
expiresIn: number;
2326
}

src/auth/interfaces/auth-result.interface.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,5 @@ export interface IAuthResult {
2121
user: IUser;
2222
accessToken: string;
2323
refreshToken: string;
24+
expiresIn: number;
2425
}

src/auth/mappers/auth-response.mapper.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,28 @@ export class AuthResponseMapper implements IAuthResponse {
3535
})
3636
public readonly accessToken: string;
3737

38+
@ApiProperty({
39+
description: 'Refresh token',
40+
example:
41+
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c',
42+
type: String,
43+
})
44+
public readonly refreshToken: string;
45+
46+
@ApiProperty({
47+
description: 'Token type',
48+
example: 'Bearer',
49+
type: String,
50+
})
51+
public readonly tokenType: string;
52+
53+
@ApiProperty({
54+
description: 'Expiration period in seconds',
55+
example: 3600,
56+
type: Number,
57+
})
58+
public readonly expiresIn: number;
59+
3860
constructor(values: IAuthResponse) {
3961
Object.assign(this, values);
4062
}
@@ -43,6 +65,9 @@ export class AuthResponseMapper implements IAuthResponse {
4365
return new AuthResponseMapper({
4466
user: AuthResponseUserMapper.map(result.user),
4567
accessToken: result.accessToken,
68+
refreshToken: result.refreshToken,
69+
tokenType: 'Bearer',
70+
expiresIn: result.expiresIn,
4671
});
4772
}
4873
}

src/common/common.service.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
Logger,
2525
LoggerService,
2626
NotFoundException,
27+
UnauthorizedException,
2728
} from '@nestjs/common';
2829
import { validate } from 'class-validator';
2930
import slugify from 'slugify';
@@ -131,6 +132,20 @@ export class CommonService {
131132
}
132133
}
133134

135+
/**
136+
* Throw Unauthorized
137+
*
138+
* Function to abstract throwing unauthorized exceptionm
139+
*/
140+
public async throwUnauthorizedError<T>(promise: Promise<T>): Promise<T> {
141+
try {
142+
return await promise;
143+
} catch (error) {
144+
this.loggerService.error(error);
145+
throw new UnauthorizedException();
146+
}
147+
}
148+
134149
/**
135150
* Format Name
136151
*

0 commit comments

Comments
 (0)