-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'feature/plugin-auth' into develop
- Loading branch information
Showing
12 changed files
with
355 additions
and
30 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,99 @@ | ||
import { BadRequestException, Body, Controller, Get, HttpCode, NotFoundException, Param, Post, Res, UnauthorizedException } from '@nestjs/common'; | ||
import { ApiExcludeController } from '@nestjs/swagger'; | ||
import { Response } from 'express'; | ||
|
||
import { User } from '../auth/auth.decorator'; | ||
import getAppConfig from '../config'; | ||
import logger from '../logger'; | ||
import { UserDocument } from '../user/user.model'; | ||
|
||
import { PluginTokenService } from './plugin-token.service'; | ||
|
||
const { publicUrl } = getAppConfig(); | ||
|
||
@ApiExcludeController() | ||
@Controller('/api/plugin-token') | ||
export class PluginTokenController { | ||
constructor( | ||
private pluginTokenService: PluginTokenService, | ||
) {} | ||
|
||
@Post('start') | ||
async startPluginAuthFlow() { | ||
const token = await this.pluginTokenService.startPluginAuthFlow(); | ||
|
||
return { | ||
userRedirectUrl: `${publicUrl}/api/plugin-token/authorize/${token._id}`, | ||
pollingUrl: `${publicUrl}/api/plugin-token/poll/${token._id}`, | ||
pollingSecret: token.pollingSecret, | ||
}; | ||
} | ||
|
||
/** | ||
* Endpoint for user to actually authorize plugin token | ||
* @param tokenId | ||
* @param user | ||
* @returns | ||
*/ | ||
@HttpCode(200) | ||
@Post('/authorize/:id') | ||
async authorizePluginToken(@Param('id') tokenId: string, @User() user: UserDocument, @Body('confirm') confirmation: string, @Body('label') label: string) { | ||
if (!user) { | ||
throw new UnauthorizedException(); | ||
} | ||
|
||
if (confirmation !== 'yes') { | ||
throw new BadRequestException(); | ||
} | ||
|
||
const wasAuthorized = await this.pluginTokenService.userAuthorizeToken(user._id, tokenId, label); | ||
|
||
if (!wasAuthorized) { | ||
throw new NotFoundException(); | ||
} | ||
|
||
return { | ||
tokenId: tokenId, | ||
userId: user._id, | ||
}; | ||
} | ||
|
||
/** | ||
* Endpoint for plugin to redirect user to, redirects to either frontend or authentication endpoints | ||
* @param id | ||
* @param res | ||
* @param user | ||
* @returns | ||
*/ | ||
@Get('/authorize/:id') | ||
async startAuthorizePluginToken(@Param('id') id: string, @Res() res: Response, @User() user: UserDocument) { | ||
logger.debug('id: %s', id); | ||
|
||
const isFlowValid = await this.pluginTokenService.isFlowIdValid(id); | ||
|
||
if (!isFlowValid) { | ||
return res.redirect('/authorize-plugin-forbidden'); | ||
} | ||
|
||
const url = `/authorize-plugin/${id}`; | ||
|
||
if (user) { | ||
return res.redirect(url); | ||
} else { | ||
return res.redirect(`/api/auth?state=${encodeURI(url)}`); | ||
} | ||
} | ||
|
||
@HttpCode(200) | ||
@Post('/poll/:id') | ||
async pollingPluginToken(@Param('id') id: string, @Body('secret') pollingSecret: string) { | ||
logger.debug('id: %s, pollingSecret: %s', id, pollingSecret); | ||
|
||
const token = await this.pluginTokenService.exchangePollingSecretForToken(id, pollingSecret); | ||
|
||
return { | ||
ready: !!token, | ||
token, | ||
}; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
import mongoose, { HydratedDocument, Model } from 'mongoose'; | ||
import ms from 'ms'; | ||
|
||
import { DB_PROVIDER } from '../database.module'; | ||
|
||
import PluginToken from '@/shared/interfaces/plugin-token.interface'; | ||
|
||
export const PLUGINTOKEN_MODEL = 'PLUGINTOKEN_MODEL'; | ||
export type PluginTokenModel = Model<PluginToken>; | ||
export type PluginTokenDocument = HydratedDocument<PluginToken>; | ||
|
||
const PluginTokenSchema = new mongoose.Schema<PluginToken>({ | ||
user: { type: String }, | ||
label: { type: String, default: 'New token' }, | ||
pollingSecret: { type: String, required: true }, | ||
token: { type: String, required: true }, | ||
lastUsed: { type: Date, default: Date.now }, | ||
}, { timestamps: true }); | ||
|
||
PluginTokenSchema.index({ lastUsed: 1 }, { expireAfterSeconds: ms('30d') / 1000 }); | ||
|
||
export const PluginTokenProvider = { | ||
provide: PLUGINTOKEN_MODEL, | ||
useFactory: (connection: typeof mongoose) => connection.model('PluginToken', PluginTokenSchema), | ||
inject: [DB_PROVIDER], | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
import { Module } from '@nestjs/common'; | ||
|
||
import { DatabaseModule } from '../database.module'; | ||
import { UtilsModule } from '../utils/utils.module'; | ||
|
||
import { PluginTokenController } from './plugin-token.controller'; | ||
import { PluginTokenProvider } from './plugin-token.model'; | ||
import { PluginTokenService } from './plugin-token.service'; | ||
|
||
@Module({ | ||
imports: [DatabaseModule, UtilsModule], | ||
providers: [PluginTokenService, PluginTokenProvider], | ||
exports: [PluginTokenService], | ||
controllers: [PluginTokenController], | ||
}) | ||
export class PluginTokenModule {} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
import { Inject, Injectable } from '@nestjs/common'; | ||
import mongoose from 'mongoose'; | ||
|
||
import { UtilsService } from '../utils/utils.service'; | ||
|
||
import { PLUGINTOKEN_MODEL, PluginTokenDocument, PluginTokenModel } from './plugin-token.model'; | ||
|
||
@Injectable() | ||
export class PluginTokenService { | ||
constructor( | ||
private utilsService: UtilsService, | ||
@Inject(PLUGINTOKEN_MODEL) private pluginTokenModel: PluginTokenModel, | ||
) {} | ||
|
||
/** | ||
* start new authentication flow for plugin auth | ||
* @returns new plugin token document | ||
*/ | ||
async startPluginAuthFlow(): Promise<PluginTokenDocument> { | ||
const token = await this.pluginTokenModel.create({ | ||
token: this.utilsService.generateRandomBytes(), | ||
pollingSecret: this.utilsService.generateRandomBytes(), | ||
}); | ||
|
||
return token; | ||
} | ||
|
||
/** | ||
* exchange pollingsecret for token, once user has finished authentication and consent | ||
* @param pollingSecret pollingsecret provided by plugin | ||
* @returns token or undefined if pollingsecret not valid | ||
*/ | ||
async exchangePollingSecretForToken(id: string, pollingSecret: string): Promise<string | undefined> { | ||
const pluginToken = await this.pluginTokenModel.findOneAndUpdate({ | ||
_id: id, | ||
pollingSecret, | ||
user: { $not: { $eq: null } }, | ||
}, { | ||
$set: { | ||
pollingSecret: null, | ||
}, | ||
}); | ||
|
||
return pluginToken?.token; | ||
} | ||
|
||
/** | ||
* get user id from token | ||
* @param token token for plugin authentication | ||
* @returns user id if token is valid, undefined otherwise | ||
*/ | ||
async attemptPluginAuthentication(token: string): Promise<string | undefined> { | ||
const pluginToken = await this.pluginTokenModel.findOneAndUpdate({ | ||
token, | ||
pollingSecret: null, | ||
}, { | ||
$set: { | ||
lastUsed: Date.now(), | ||
}, | ||
}); | ||
|
||
return pluginToken?.user; | ||
} | ||
|
||
async userAuthorizeToken(userId: string | mongoose.Types.ObjectId, tokenId: string, label = 'Token'): Promise<boolean> { | ||
const pluginToken = await this.pluginTokenModel.findOneAndUpdate({ | ||
_id: tokenId, | ||
}, { | ||
$set: { | ||
user: userId, | ||
label, | ||
}, | ||
}); | ||
|
||
return !!pluginToken; | ||
} | ||
|
||
async isFlowIdValid(id: string): Promise<boolean> { | ||
const count = await this.pluginTokenModel.count({ _id: id }); | ||
|
||
return count > 0; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.