Skip to content

Commit

Permalink
refactor: clean auth module
Browse files Browse the repository at this point in the history
  • Loading branch information
Behzad-rabiei committed Aug 30, 2024
1 parent 8c43927 commit f03ed79
Show file tree
Hide file tree
Showing 23 changed files with 202 additions and 101 deletions.
1 change: 0 additions & 1 deletion .husky/pre-push
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
npm test
npx lint - staged
6 changes: 3 additions & 3 deletions src/auth-discord/auth-discord-controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { AuthDiscordController } from './auth-discord.controller'
import { OAuthService } from '../auth/oAuth.service'
import { CryptoUtilsService } from '../utils/crypto-utils.service'
import { HttpStatus, ForbiddenException } from '@nestjs/common'
import { AUTH_PROVIDERS } from '../auth/constants/provider.constants'
import { OAUTH_METHODS } from '../auth/constants/auth.constants'

describe('AuthDiscordController', () => {
let controller: AuthDiscordController
Expand Down Expand Up @@ -41,7 +41,7 @@ describe('AuthDiscordController', () => {
expect(mockSession.state).toEqual('mock-state')
expect(mockCryptoService.generateState).toHaveBeenCalled()
expect(mockOAuthService.generateRedirectUrl).toHaveBeenCalledWith(
AUTH_PROVIDERS.DISCORD,
OAUTH_METHODS.DISCORD,
'mock-state'
)
})
Expand All @@ -65,7 +65,7 @@ describe('AuthDiscordController', () => {
'mock-state',
'mock-state',
'valid-code',
AUTH_PROVIDERS.DISCORD
OAUTH_METHODS.DISCORD
)
})
it('should throw HttpException if state is invalid', async () => {
Expand Down
10 changes: 5 additions & 5 deletions src/auth-discord/auth-discord.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@ import {
import { OAuthService } from '../auth/oAuth.service'
import { HandleOAuthCallback } from './dto/handle-oauth-callback-dto'
import { CryptoUtilsService } from '../utils/crypto-utils.service'
import { AUTH_PROVIDERS } from '../auth/constants/provider.constants'
import { OAUTH_METHODS } from '../auth/constants/auth.constants'
import { JwtResponse } from '../auth//dto/jwt-response.dto'

@ApiTags(`${AUTH_PROVIDERS.DISCORD} Authentication`)
@Controller(`auth/${AUTH_PROVIDERS.DISCORD}`)
@ApiTags(`${OAUTH_METHODS.DISCORD} Authentication`)
@Controller(`auth/${OAUTH_METHODS.DISCORD}`)
export class AuthDiscordController {
constructor(
private readonly oAuthService: OAuthService,
Expand All @@ -33,7 +33,7 @@ export class AuthDiscordController {
redirectToDiscord(@Session() session: any) {
const state = this.cryptoService.generateState()
const url = this.oAuthService.generateRedirectUrl(
AUTH_PROVIDERS.DISCORD,
OAUTH_METHODS.DISCORD,
state
)
session.state = state
Expand All @@ -55,7 +55,7 @@ export class AuthDiscordController {
state,
session.state,
code,
AUTH_PROVIDERS.DISCORD
OAUTH_METHODS.DISCORD
)
return {
url: redirectUrl,
Expand Down
6 changes: 3 additions & 3 deletions src/auth-google/auth-google.controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { AuthGoogleController } from './auth-google.controller'
import { OAuthService } from '../auth/oAuth.service'
import { CryptoUtilsService } from '../utils/crypto-utils.service'
import { HttpStatus, ForbiddenException } from '@nestjs/common'
import { AUTH_PROVIDERS } from '../auth/constants/provider.constants'
import { OAUTH_METHODS } from '../auth/constants/auth.constants'

describe('AuthGoogleController', () => {
let controller: AuthGoogleController
Expand Down Expand Up @@ -41,7 +41,7 @@ describe('AuthGoogleController', () => {
expect(mockSession.state).toEqual('mock-state')
expect(mockCryptoService.generateState).toHaveBeenCalled()
expect(mockOAuthService.generateRedirectUrl).toHaveBeenCalledWith(
AUTH_PROVIDERS.GOOGLE,
OAUTH_METHODS.GOOGLE,
'mock-state'
)
})
Expand All @@ -65,7 +65,7 @@ describe('AuthGoogleController', () => {
'mock-state',
'mock-state',
'valid-code',
AUTH_PROVIDERS.GOOGLE
OAUTH_METHODS.GOOGLE
)
})

Expand Down
11 changes: 6 additions & 5 deletions src/auth-google/auth-google.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,12 @@ import {
} from '@nestjs/swagger'
import { HandleOAuthCallback } from './dto/handle-oauth-callback-dto'
import { JwtResponse } from '../auth/dto/jwt-response.dto'
import { AUTH_PROVIDERS } from '../auth/constants/provider.constants'
import { OAUTH_METHODS } from '../auth/constants/auth.constants'
import { CryptoUtilsService } from '../utils/crypto-utils.service'
import { OAuthService } from '../auth/oAuth.service'
@ApiTags(`${AUTH_PROVIDERS.GOOGLE} Authentication`)
@Controller(`auth/${AUTH_PROVIDERS.GOOGLE}`)

@ApiTags(`${OAUTH_METHODS.GOOGLE} Authentication`)
@Controller(`auth/${OAUTH_METHODS.GOOGLE}`)
export class AuthGoogleController {
constructor(
private readonly oAuthService: OAuthService,
Expand All @@ -32,7 +33,7 @@ export class AuthGoogleController {
redirectToGoogle(@Session() session: any) {
const state = this.cryptoService.generateState()
const url = this.oAuthService.generateRedirectUrl(
AUTH_PROVIDERS.GOOGLE,
OAUTH_METHODS.GOOGLE,
state
)
session.state = state
Expand All @@ -54,7 +55,7 @@ export class AuthGoogleController {
state,
session.state,
code,
AUTH_PROVIDERS.GOOGLE
OAUTH_METHODS.GOOGLE
)

return {
Expand Down
10 changes: 5 additions & 5 deletions src/auth-siwe/auth-siwe.controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { AuthSiweController } from './auth-siwe.controller'
import { SiweService } from './siwe.service'
import { AuthService } from '../auth/auth.service'
import { VerifySiweDto } from './dto/verify-siwe.dto'
import { AUTH_PROVIDERS } from '../auth/constants/provider.constants'
import { AUTH_METHODS } from '../auth/constants/auth.constants'
import { parseSiweMessage } from 'viem/siwe'

jest.mock('viem/siwe', () => ({
Expand All @@ -29,7 +29,7 @@ describe('AuthSiweController', () => {
{
provide: AuthService,
useValue: {
generateJwt: jest.fn(),
generateUserJWT: jest.fn(),
},
},
],
Expand Down Expand Up @@ -66,7 +66,7 @@ describe('AuthSiweController', () => {
jest.spyOn(siweService, 'verifySiweMessage').mockResolvedValue(
undefined
)
jest.spyOn(authService, 'generateJwt').mockResolvedValue(jwt)
jest.spyOn(authService, 'generateUserJWT').mockResolvedValue(jwt)
;(parseSiweMessage as jest.Mock).mockReturnValue({ address })

const result = await controller.verifySiwe(verifySiweDto)
Expand All @@ -77,9 +77,9 @@ describe('AuthSiweController', () => {
verifySiweDto.signature,
verifySiweDto.chainId
)
expect(authService.generateJwt).toHaveBeenCalledWith(
expect(authService.generateUserJWT).toHaveBeenCalledWith(
address,
AUTH_PROVIDERS.SIWE
AUTH_METHODS.SIWE
)
})
})
Expand Down
11 changes: 6 additions & 5 deletions src/auth-siwe/auth-siwe.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,14 @@ import {
} from '@nestjs/swagger'
import { AuthService } from '../auth/auth.service'
import { VerifySiweDto } from './dto/verify-siwe.dto'
import { AUTH_PROVIDERS } from '../auth/constants/provider.constants'
import { JwtResponse } from '../auth//dto/jwt-response.dto'
import { parseSiweMessage } from 'viem/siwe'
import { NonceResponse } from './dto/nonce.dto'
import { JWT_PROVIDERS } from '../auth/constants/jwt.constants'
import { AUTH_METHODS } from 'src/auth/constants/auth.constants'

@ApiTags(`${AUTH_PROVIDERS.SIWE} Authentication`)
@Controller(`auth/${AUTH_PROVIDERS.SIWE}`)
@ApiTags(`${JWT_PROVIDERS.SIWE} Authentication`)
@Controller(`auth/${AUTH_METHODS.SIWE}`)
export class AuthSiweController {
constructor(
private readonly siweService: SiweService,
Expand Down Expand Up @@ -49,9 +50,9 @@ export class AuthSiweController {
async verifySiwe(@Body() verifySiweDto: VerifySiweDto) {
const { message, signature, chainId } = verifySiweDto
await this.siweService.verifySiweMessage(message, signature, chainId)
const jwt = await this.authService.generateJwt(
const jwt = this.authService.generateUserJWT(
parseSiweMessage(message).address,
AUTH_PROVIDERS.SIWE
AUTH_METHODS.SIWE
)
return { jwt }
}
Expand Down
4 changes: 2 additions & 2 deletions src/auth-siwe/siwe.service.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Injectable, HttpException, HttpStatus } from '@nestjs/common'
import { PinoLogger, InjectPinoLogger } from 'nestjs-pino'
import { AUTH_PROVIDERS } from '../auth/constants/provider.constants'
import { AUTH_METHODS } from '../auth/constants/auth.constants'
import { generateSiweNonce } from 'viem/siwe'
import { ViemUtilsService } from '../utils/viem.utils.service'
import { Hex } from 'viem'
Expand Down Expand Up @@ -30,7 +30,7 @@ export class SiweService {
} catch (error) {
this.logger.error(error, `Siwe Verification Failed`)
throw new HttpException(
`${AUTH_PROVIDERS.SIWE} verification Failed`,
`${AUTH_METHODS.SIWE} verification Failed`,
HttpStatus.BAD_REQUEST
)
}
Expand Down
40 changes: 27 additions & 13 deletions src/auth/auth.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,29 @@ import { JwtService } from '@nestjs/jwt'
import { AuthService } from './auth.service'
import { ConfigService } from '@nestjs/config'
import * as moment from 'moment'
import { JwtPayload } from './types/jwt-payload.type'
import { JwtPayload } from './types/jwt.type'
import * as jwt from 'jsonwebtoken'
import { PinoLogger, LoggerModule } from 'nestjs-pino'
import { UnauthorizedException } from '@nestjs/common'
import { JWT_TYPES } from './constants/jwt.constants'
import { privateKeyToAddress } from 'viem/accounts'

const mockPublicKey =
'0xabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdef'
const mockPrivateKey =
'0xa45f7276817eea015efd2900aafaa1b500b00cce790fca8222bb499c5fc96531'
const mockJwtSecret = 'jwtSecret'
const mockJwtExpiresIn = '60'
const mockJwtAlgorithm = 'HS256'
const mockJwtUserExpirationMinutes = '60'
const mockJwtdiscourseVerificationExpirationMinutes = '10'

const mockConfigService = {
get: jest.fn((key: string) => {
if (key === 'wallet.publicKey') return mockPublicKey
if (key === 'wallet.privateKey') return mockPrivateKey
if (key === 'jwt.secret') return mockJwtSecret
if (key === 'jwt.expiresIn') return mockJwtExpiresIn
if (key === 'jwt.algorithm') return mockJwtAlgorithm
if (key === 'jwt.userExpirationMinutes')
return mockJwtUserExpirationMinutes
if (key === 'jwt.discourseVerificationExpirationMinutes')
return mockJwtdiscourseVerificationExpirationMinutes
}),
}

Expand Down Expand Up @@ -48,16 +56,19 @@ describe('AuthService', () => {
describe('signPayload', () => {
it('should return a valid JWT', async () => {
const payload: JwtPayload = {
type: JWT_TYPES.USER,
sub: '1',
provider: 'google',
iat: moment().unix(),
exp: moment().add(mockJwtExpiresIn, 'minutes').unix(),
iss: mockPublicKey,
exp: moment()
.add(mockJwtUserExpirationMinutes, 'minutes')
.unix(),
iss: privateKeyToAddress(mockPrivateKey),
}
const token = await service.signPayload(payload)
expect(typeof token).toBe('string')
const decoded = jwt.verify(token, mockJwtSecret, {
algorithms: ['HS256'],
algorithms: [mockJwtAlgorithm],
})
expect(decoded).toMatchObject(payload)
})
Expand All @@ -66,20 +77,23 @@ describe('AuthService', () => {
describe('validateToken', () => {
it('should validate a token correctly', async () => {
const payload: JwtPayload = {
type: JWT_TYPES.USER,
sub: '1',
provider: 'google',
iat: moment().unix(),
exp: moment().add(mockJwtExpiresIn, 'minutes').unix(),
iss: mockPublicKey,
exp: moment()
.add(mockJwtUserExpirationMinutes, 'minutes')
.unix(),
iss: privateKeyToAddress(mockPrivateKey),
}
const token = jwt.sign(payload, mockJwtSecret, {
algorithm: 'HS256',
algorithm: mockJwtAlgorithm,
})
const decoded = await service.validateToken(token)
expect(decoded).toMatchObject(payload)
})

it('should return null if token is invalid', async () => {
it('should return UnauthorizedException if token is invalid', async () => {
await expect(
service.validateToken('invalid.token.here')
).rejects.toThrow(UnauthorizedException)
Expand Down
66 changes: 50 additions & 16 deletions src/auth/auth.service.ts
Original file line number Diff line number Diff line change
@@ -1,50 +1,84 @@
import { Injectable, UnauthorizedException } from '@nestjs/common'
import * as jwt from 'jsonwebtoken'
import { JwtPayload } from './types/jwt-payload.type'
import { Algorithm } from 'jsonwebtoken'
import { JwtPayload, JwtProvider } from './types/jwt.type'
import { ConfigService } from '@nestjs/config'
import { PinoLogger, InjectPinoLogger } from 'nestjs-pino'
import * as moment from 'moment'
import { JWT_TYPES } from './constants/jwt.constants'
import { Address } from 'viem'
import { privateKeyToAddress } from 'viem/accounts'

@Injectable()
export class AuthService {
private secret: string
private algorithm: Algorithm
public userExpirationMinutes: number
public discourseVerificationExpirationMinutes: number
public appAddress: Address
constructor(
private readonly configService: ConfigService,
@InjectPinoLogger(AuthService.name)
private readonly logger: PinoLogger
) {}
) {
this.secret = this.configService.get<string>('jwt.secret')
this.algorithm = this.configService.get<Algorithm>('jwt.algorithm')
this.userExpirationMinutes = this.configService.get<number>(
'jwt.userExpirationMinutes'
)
this.discourseVerificationExpirationMinutes =
this.configService.get<number>(
'jwt.discourseVerificationExpirationMinutes'
)
this.appAddress = privateKeyToAddress(
this.configService.get<'0x${string}'>('wallet.privateKey')
)
}

async signPayload(payload: JwtPayload): Promise<string> {
return jwt.sign(payload, this.configService.get<string>('jwt.secret'), {
algorithm: 'HS256',
signPayload(payload: JwtPayload): string {
return jwt.sign(payload, this.secret, {
algorithm: this.algorithm,
})
}

async validateToken(token: string): Promise<JwtPayload> {
validateToken(token: string): JwtPayload {
try {
return jwt.verify(
token,
this.configService.get<string>('jwt.secret'),
{
algorithms: ['HS256'],
}
) as JwtPayload
return jwt.verify(token, this.secret, {
algorithms: [this.algorithm],
}) as JwtPayload
} catch (error) {
this.logger.error(error, `Failed to validtae token`)
throw new UnauthorizedException(error.message)
}
}

async generateJwt(identifier: string, provider: string): Promise<string> {
generateUserJWT(identifier: string, provider: JwtProvider): string {
const now = moment().unix()
const expiration = moment()
.add(this.configService.get<string>('jwt.expiresIn'), 'minutes')
.add(this.userExpirationMinutes, 'minutes')
.unix()
const payload: JwtPayload = {
type: JWT_TYPES.USER,
sub: identifier,
provider,
iat: now,
exp: expiration,
iss: this.configService.get<string>('wallet.publicKey'),
iss: this.appAddress,
}
return this.signPayload(payload)
}

generateDiscourseVerificationJWT(code: string): string {
const now = moment().unix()
const expiration = moment()
.add(this.discourseVerificationExpirationMinutes, 'minutes')
.unix()
const payload: JwtPayload = {
type: JWT_TYPES.DISCOURSE_VERIFICATION,
sub: code,
iat: now,
exp: expiration,
iss: this.appAddress,
}
return this.signPayload(payload)
}
Expand Down
Loading

0 comments on commit f03ed79

Please sign in to comment.