-
Notifications
You must be signed in to change notification settings - Fork 26
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(authentication): passwordless login (#346)
- Loading branch information
1 parent
b0db3b9
commit 7c9df4b
Showing
7 changed files
with
176 additions
and
0 deletions.
There are no files selected for viewing
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,13 @@ | ||
export default { | ||
magic_link: { | ||
enabled: { | ||
format: 'Boolean', | ||
default: false, | ||
}, | ||
redirect_uri: { | ||
format: 'String', | ||
default: '', | ||
optional: true, | ||
}, | ||
}, | ||
}; |
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,143 @@ | ||
import { isNil } from 'lodash'; | ||
import { TokenType } from '../constants/TokenType'; | ||
import { v4 as uuid } from 'uuid'; | ||
import ConduitGrpcSdk, { | ||
ConduitRouteActions, | ||
ConduitRouteReturnDefinition, | ||
ConduitString, | ||
ConfigController, | ||
Email, | ||
GrpcError, | ||
ParsedRouterRequest, | ||
RoutingManager, | ||
UnparsedRouterResponse, | ||
} from '@conduitplatform/grpc-sdk'; | ||
import { Token, User } from '../models'; | ||
import { status } from '@grpc/grpc-js'; | ||
import { IAuthenticationStrategy } from '../interfaces/AuthenticationStrategy'; | ||
import { TokenProvider } from './tokenProvider'; | ||
import { MagicLinkTemplate as magicLinkTemplate } from '../templates'; | ||
|
||
export class MagicLinkHandlers implements IAuthenticationStrategy { | ||
private emailModule: Email; | ||
private initialized: boolean = false; | ||
|
||
constructor(private readonly grpcSdk: ConduitGrpcSdk) {} | ||
|
||
async validate(): Promise<boolean> { | ||
const config = ConfigController.getInstance().config; | ||
if (config.magic_link.enabled && this.grpcSdk.isAvailable('email')) { | ||
this.emailModule = this.grpcSdk.emailProvider!; | ||
const success = await this.registerTemplate() | ||
.then(() => true) | ||
.catch(e => { | ||
ConduitGrpcSdk.Logger.error(e); | ||
return false; | ||
}); | ||
return (this.initialized = success); | ||
} else { | ||
return (this.initialized = false); | ||
} | ||
} | ||
|
||
async declareRoutes(routingManager: RoutingManager): Promise<void> { | ||
routingManager.route( | ||
{ | ||
path: '/magic-link', | ||
action: ConduitRouteActions.POST, | ||
description: `Send magic link to a user.`, | ||
bodyParams: { | ||
email: ConduitString.Required, | ||
}, | ||
}, | ||
new ConduitRouteReturnDefinition('MagicLinkSendResponse', 'String'), | ||
this.sendMagicLink.bind(this), | ||
); | ||
|
||
routingManager.route( | ||
{ | ||
path: '/hook/magic-link/:verificationToken', | ||
action: ConduitRouteActions.GET, | ||
description: `A webhook used to verify a user who has received a magic link.`, | ||
urlParams: { | ||
verificationToken: ConduitString.Required, | ||
}, | ||
}, | ||
new ConduitRouteReturnDefinition('VerifyMagicLinkLoginResponse', { | ||
accessToken: ConduitString.Optional, | ||
refreshToken: ConduitString.Optional, | ||
}), | ||
this.verifyLogin.bind(this), | ||
); | ||
} | ||
|
||
async sendMagicLink(call: ParsedRouterRequest): Promise<UnparsedRouterResponse> { | ||
const { email } = call.request.params; | ||
const user: User | null = await User.getInstance().findOne({ email: email }); | ||
if (isNil(user)) throw new GrpcError(status.NOT_FOUND, 'User not found'); | ||
|
||
const token: Token = await Token.getInstance().create({ | ||
type: TokenType.MAGIC_LINK, | ||
user: user._id, | ||
token: uuid(), | ||
}); | ||
|
||
await this.sendMagicLinkMail(user, token); | ||
return 'token sent'; | ||
} | ||
|
||
private async sendMagicLinkMail(user: User, token: Token) { | ||
const serverConfig = await this.grpcSdk.config.get('router'); | ||
const url = serverConfig.hostUrl; | ||
|
||
const result = { token, hostUrl: url }; | ||
const link = `${result.hostUrl}/hook/authentication/magic-link/${result.token.token}`; | ||
await this.emailModule.sendEmail('MagicLink', { | ||
email: user.email, | ||
sender: 'no-reply', | ||
variables: { | ||
link, | ||
}, | ||
}); | ||
return 'Email sent'; | ||
} | ||
|
||
async verifyLogin(call: ParsedRouterRequest): Promise<UnparsedRouterResponse> { | ||
const { verificationToken } = call.request.params.verificationToken; | ||
const context = call.request.context; | ||
|
||
const clientId = context.clientId; | ||
const config = ConfigController.getInstance().config; | ||
const redirectUri = config.magic_link.redirect_uri; | ||
const token: Token | null = await Token.getInstance().findOne({ | ||
type: TokenType.MAGIC_LINK, | ||
token: verificationToken, | ||
}); | ||
if (isNil(token)) { | ||
throw new GrpcError(status.NOT_FOUND, 'Magic link token does not exist'); | ||
} | ||
const user: User | null = await User.getInstance().findOne({ | ||
_id: token.user! as string, | ||
}); | ||
if (isNil(user)) throw new GrpcError(status.NOT_FOUND, 'User not found'); | ||
|
||
await Token.getInstance() | ||
.deleteMany({ user: token.user, type: TokenType.MAGIC_LINK }) | ||
.catch(e => { | ||
ConduitGrpcSdk.Logger.error(e); | ||
}); | ||
|
||
return TokenProvider.getInstance()!.provideUserTokens( | ||
{ | ||
user, | ||
clientId, | ||
config, | ||
}, | ||
redirectUri, | ||
); | ||
} | ||
|
||
private registerTemplate() { | ||
return this.emailModule.registerTemplate(magicLinkTemplate); | ||
} | ||
} |
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,6 @@ | ||
export const MagicLinkTemplate = { | ||
name: 'MagicLink', | ||
subject: 'Login', | ||
body: 'Click <a href="{{link}}">here</a> to login', | ||
variables: ['link'], | ||
}; |
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 |
---|---|---|
@@ -1,3 +1,4 @@ | ||
export * from './ForgotPassword'; | ||
export * from './VerifyEmail'; | ||
export * from './VerifyChangeEmail'; | ||
export * from './MagicLink'; |