Skip to content

Commit

Permalink
feat(api): Added GitLab OAuth (#188)
Browse files Browse the repository at this point in the history
  • Loading branch information
rajdip-b authored Apr 20, 2024
1 parent 3dc0c4c commit 4d3bbe4
Show file tree
Hide file tree
Showing 11 changed files with 216 additions and 17 deletions.
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
GOOGLE_CALLBACK_URL=

GITLAB_CLIENT_ID=
GITLAB_CLIENT_SECRET=
GITLAB_CALLBACK_URL=

SENTRY_DSN=
SENTRY_ORG=
SENTRY_PROJECT=
Expand Down
1 change: 1 addition & 0 deletions apps/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"moment": "^2.30.1",
"nodemailer": "^6.9.9",
"passport-github2": "^0.1.12",
"passport-gitlab2": "^5.0.0",
"passport-google-oauth20": "^2.0.0",
"prisma": "^5.10.1",
"redis": "^4.6.13",
Expand Down
10 changes: 10 additions & 0 deletions apps/api/src/auth/auth.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import { GithubStrategy } from '../config/oauth-strategy/github/github.strategy'
import { GithubOAuthStrategyFactory } from '../config/factory/github/github-strategy.factory'
import { GoogleOAuthStrategyFactory } from '../config/factory/google/google-strategy.factory'
import { GoogleStrategy } from '../config/oauth-strategy/google/google.strategy'
import { GitlabOAuthStrategyFactory } from '../config/factory/gitlab/gitlab-strategy.factory'
import { GitlabStrategy } from '../config/oauth-strategy/gitlab/gitlab.strategy'

@Module({
imports: [
Expand Down Expand Up @@ -38,6 +40,14 @@ import { GoogleStrategy } from '../config/oauth-strategy/google/google.strategy'
googleOAuthStrategyFactory.createOAuthStrategy()
},
inject: [GoogleOAuthStrategyFactory]
},
GitlabOAuthStrategyFactory,
{
provide: GitlabStrategy,
useFactory: (gitlabOAuthStrategyFactory: GitlabOAuthStrategyFactory) => {
gitlabOAuthStrategyFactory.createOAuthStrategy()
},
inject: [GitlabOAuthStrategyFactory]
}
],
controllers: [AuthController]
Expand Down
2 changes: 2 additions & 0 deletions apps/api/src/auth/controller/auth.controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { mockDeep } from 'jest-mock-extended'
import { ConfigService } from '@nestjs/config'
import { GithubOAuthStrategyFactory } from '../../config/factory/github/github-strategy.factory'
import { GoogleOAuthStrategyFactory } from '../../config/factory/google/google-strategy.factory'
import { GitlabOAuthStrategyFactory } from '../../config/factory/gitlab/gitlab-strategy.factory'

describe('AuthController', () => {
let controller: AuthController
Expand All @@ -20,6 +21,7 @@ describe('AuthController', () => {
AuthService,
GithubOAuthStrategyFactory,
GoogleOAuthStrategyFactory,
GitlabOAuthStrategyFactory,
ConfigService,
{ provide: MAIL_SERVICE, useClass: MockMailService },
JwtService,
Expand Down
51 changes: 50 additions & 1 deletion apps/api/src/auth/controller/auth.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,16 @@ import {
import { AuthGuard } from '@nestjs/passport'
import { GithubOAuthStrategyFactory } from '../../config/factory/github/github-strategy.factory'
import { GoogleOAuthStrategyFactory } from '../../config/factory/google/google-strategy.factory'
import { GitlabOAuthStrategyFactory } from '../../config/factory/gitlab/gitlab-strategy.factory'

@ApiTags('Auth Controller')
@Controller('auth')
export class AuthController {
constructor(
private authService: AuthService,
private githubOAuthStrategyFactory: GithubOAuthStrategyFactory,
private googleOAuthStrategyFactory: GoogleOAuthStrategyFactory
private googleOAuthStrategyFactory: GoogleOAuthStrategyFactory,
private gitlabOAuthStrategyFactory: GitlabOAuthStrategyFactory
) {}

@Public()
Expand Down Expand Up @@ -145,6 +147,53 @@ export class AuthController {
)
}

/* istanbul ignore next */
@Public()
@Get('gitlab')
@ApiOperation({
summary: 'Gitlab OAuth',
description:
'This endpoint validates Gitlab OAuth. If the OAuth is valid, it returns a valid token along with the user details'
})
async gitlabOAuthLogin(@Res() res) {
if (!this.gitlabOAuthStrategyFactory.isOAuthEnabled()) {
throw new HttpException(
'GitLab Auth is not enabled in this environment. Refer to the https://docs.keyshade.xyz/contributing-to-keyshade/environment-variables if you would like to set it up.',
HttpStatus.BAD_REQUEST
)
}

res.status(302).redirect('/api/auth/gitlab/callback')
}

/* istanbul ignore next */
@Public()
@Get('gitlab/callback')
@UseGuards(AuthGuard('gitlab'))
@ApiOperation({
summary: 'Gitlab OAuth Callback',
description:
'This endpoint validates Gitlab OAuth. If the OAuth is valid, it returns a valid token along with the user details'
})
@ApiParam({
name: 'code',
description: 'Code for the Callback',
required: true
})
@ApiResponse({
status: HttpStatus.OK,
description: 'Logged in successfully'
})
async gitlabOAuthCallback(@Req() req) {
const { emails, displayName: name, avatarUrl: profilePictureUrl } = req.user
const email = emails[0].value
return await this.authService.handleOAuthLogin(
email,
name,
profilePictureUrl
)
}

/* istanbul ignore next */
@Public()
@Get('google')
Expand Down
46 changes: 46 additions & 0 deletions apps/api/src/config/factory/gitlab/gitlab-strategy.factory.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { Test, TestingModule } from '@nestjs/testing'
import { ConfigService } from '@nestjs/config'
import { GitlabStrategy } from '../../oauth-strategy/gitlab/gitlab.strategy'
import { GitlabOAuthStrategyFactory } from './gitlab-strategy.factory'

describe('GitlabOAuthStrategyFactory', () => {
let factory: GitlabOAuthStrategyFactory
let configService: ConfigService

beforeEach(async () => {
const moduleRef: TestingModule = await Test.createTestingModule({
providers: [{ provide: ConfigService, useValue: { get: jest.fn() } }]
}).compile()
configService = moduleRef.get<ConfigService>(ConfigService)
})

it('should disable OAuth when credentials are not present', () => {
jest.spyOn(configService, 'get').mockReturnValue('')
factory = new GitlabOAuthStrategyFactory(configService)
expect(factory.isOAuthEnabled()).toBe(false)
})

it('should return null when OAuth is disabled', () => {
const strategy = factory.createOAuthStrategy()
expect(strategy).toBeNull()
})

it('should enable OAuth when credentials are present', () => {
jest
.spyOn(configService, 'get')
.mockImplementation((key) =>
key === 'GITLAB_CLIENT_ID' ||
key === 'GITLAB_CLIENT_SECRET' ||
key === 'GITLAB_CALLBACK_URL'
? 'test'
: ''
)
factory = new GitlabOAuthStrategyFactory(configService)
expect(factory.isOAuthEnabled()).toBe(true)
})

it('should create OAuth strategy when enabled', () => {
const strategy = factory.createOAuthStrategy()
expect(strategy).toBeInstanceOf(GitlabStrategy)
})
})
35 changes: 35 additions & 0 deletions apps/api/src/config/factory/gitlab/gitlab-strategy.factory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { Injectable, Logger } from '@nestjs/common'
import { ConfigService } from '@nestjs/config'
import { OAuthStrategyFactory } from '../oauth-strategy.factory'
import { GitlabStrategy } from '../../oauth-strategy/gitlab/gitlab.strategy'

@Injectable()
export class GitlabOAuthStrategyFactory implements OAuthStrategyFactory {
private readonly clientID: string
private readonly clientSecret: string
private readonly callbackURL: string
constructor(private readonly configService: ConfigService) {
this.clientID = this.configService.get<string>('GITLAB_CLIENT_ID')
this.clientSecret = this.configService.get<string>('GITLAB_CLIENT_SECRET')
this.callbackURL = this.configService.get<string>('GITLAB_CALLBACK_URL')
}

public isOAuthEnabled(): boolean {
return Boolean(this.clientID && this.clientSecret && this.callbackURL)
}

public createOAuthStrategy<GitlabStrategy>(): GitlabStrategy | null {
if (this.isOAuthEnabled()) {
return new GitlabStrategy(
this.clientID,
this.clientSecret,
this.callbackURL
) as GitlabStrategy
} else {
Logger.warn(
'GitLab Auth is not enabled in this environment. Refer to the https://docs.keyshade.xyz/contributing-to-keyshade/environment-variables if you would like to set it up.'
)
return null
}
}
}
17 changes: 17 additions & 0 deletions apps/api/src/config/oauth-strategy/gitlab/gitlab.strategy.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { GitlabStrategy } from './gitlab.strategy'

describe('GitlabStrategy', () => {
let strategy: GitlabStrategy

beforeEach(() => {
strategy = new GitlabStrategy('clientID', 'clientSecret', 'callbackURL')
})

it('should be defined', () => {
expect(strategy).toBeDefined()
})

it('should have a validate method', () => {
expect(strategy.validate).toBeDefined()
})
})
23 changes: 23 additions & 0 deletions apps/api/src/config/oauth-strategy/gitlab/gitlab.strategy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Injectable } from '@nestjs/common'
import { PassportStrategy } from '@nestjs/passport'
import { Profile, Strategy } from 'passport-gitlab2'

@Injectable()
export class GitlabStrategy extends PassportStrategy(Strategy, 'gitlab') {
constructor(clientID: string, clientSecret: string, callbackURL: string) {
super({
clientID,
clientSecret,
callbackURL,
scope: ['read_user']
})
}

async validate(
_accessToken: string,
_refreshToken: string,
profile: Profile
): Promise<Profile> {
return profile
}
}
34 changes: 18 additions & 16 deletions docs/contributing-to-keyshade/environment-variables.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,21 @@ description: Get to know the environment you are working with

Here's the description of the environment variables used in the project. You can find the values for these variables in \`.env.example\`.

* **DATABASE\_URL**: The URL of the PSQL database to connect to. This is used by the [Prisma Client](https://www.prisma.io/docs/orm/prisma-client) to connect to the database.
* **SMTP\_HOST**: This is used to send out emails from the backend.&#x20;
* **SMTP\_PORT:** The SMTP port as specified by your SMTP provider.
* **SMTP\_EMAIL\_ADDRESS:** The email address you want to be sending out the emails from.
* **SMTP\_PASSWORD:** The app password for your email account. &#x20;
* **GITHUB\_CLIENT\_ID, GITHUB\_CLIENT\_SECRET, GITHUB\_CALLBACK\_URL:** These settings can be configured by adding an OAuth app in your GitHub account's developer section. Please note that it's not mandatory, until and unless you want to support GitHub OAuth.
* **SENTRY\_DSN**: The Data Source Name (DSN) for Sentry, a platform for monitoring, troubleshooting, and resolving issues in real-time. This is used to configure error tracking in the project.
* **SENTRY\_ORG**: The organization ID associated with your Sentry account.
* **SENTRY\_PROJECT**: The project ID within your Sentry organization where events will be reported.
* **SENTRY\_TRACES\_SAMPLE\_RATE**: The sample rate for collecting transaction traces in Sentry. It determines the percentage of transactions to capture traces for.
* **SENTRY\_PROFILES\_SAMPLE\_RATE**: The sample rate for collecting performance profiles in Sentry. It determines the percentage of requests to capture performance profiles for.
* **SENTRY\_ENV**: The The environment in which the app is running. It can be either 'development', 'production', or 'test'. Please note that it's not mandatory, it will default to "production" enviroment for the sentry configuration.
* **FROM\_EMAIL**: The display of the email sender title.
* **JWT\_SECRET**: The secret used to sign the JWT tokens. It is insignificant in the development environment.
* **WEB\_FRONTEND\_URL, WORKSPACE\_FRONTEND\_URL**: The URLs of the web and workspace frontend respectively. These are used in the emails sometimes and in other spaces of the application too.
* **API\_PORT**: The environmental variable that specifies the port number on which the API server should listen for incoming connections. If not explicitly set, it defaults to port 4200.
- **DATABASE_URL**: The URL of the PSQL database to connect to. This is used by the [Prisma Client](https://www.prisma.io/docs/orm/prisma-client) to connect to the database.
- **SMTP_HOST**: This is used to send out emails from the backend.&#x20;
- **SMTP_PORT:** The SMTP port as specified by your SMTP provider.
- **SMTP_EMAIL_ADDRESS:** The email address you want to be sending out the emails from.
- **SMTP_PASSWORD:** The app password for your email account. &#x20;
- **GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET, GITHUB_CALLBACK_URL:** These settings can be configured by adding an OAuth app in your GitHub account's developer section. Please note that it's not mandatory, until and unless you want to support GitHub OAuth.
- **GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, GOOGLE_CALLBACK_URL:** These settings can be configured by adding an OAuth app in your Google account's cloud platform. Please note that it's not mandatory, until and unless you want to support Google OAuth.
- **GITLAB_CLIENT_ID, GITLAB_CLIENT_SECRET, GITLAB_CALLBACK_URL:** These settings can be configured by adding an OAuth app in your GitLab account's application section. Please note that it's not mandatory, until and unless you want to support GitLab OAuth.
- **SENTRY_DSN**: The Data Source Name (DSN) for Sentry, a platform for monitoring, troubleshooting, and resolving issues in real-time. This is used to configure error tracking in the project.
- **SENTRY_ORG**: The organization ID associated with your Sentry account.
- **SENTRY_PROJECT**: The project ID within your Sentry organization where events will be reported.
- **SENTRY_TRACES_SAMPLE_RATE**: The sample rate for collecting transaction traces in Sentry. It determines the percentage of transactions to capture traces for.
- **SENTRY_PROFILES_SAMPLE_RATE**: The sample rate for collecting performance profiles in Sentry. It determines the percentage of requests to capture performance profiles for.
- **SENTRY_ENV**: The The environment in which the app is running. It can be either 'development', 'production', or 'test'. Please note that it's not mandatory, it will default to "production" enviroment for the sentry configuration.
- **FROM_EMAIL**: The display of the email sender title.
- **JWT_SECRET**: The secret used to sign the JWT tokens. It is insignificant in the development environment.
- **WEB_FRONTEND_URL, WORKSPACE_FRONTEND_URL**: The URLs of the web and workspace frontend respectively. These are used in the emails sometimes and in other spaces of the application too.
- **API_PORT**: The environmental variable that specifies the port number on which the API server should listen for incoming connections. If not explicitly set, it defaults to port 4200.
10 changes: 10 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 4d3bbe4

Please sign in to comment.