Skip to content

Commit

Permalink
feat(authentication): passwordless login (#346)
Browse files Browse the repository at this point in the history
  • Loading branch information
SotiriaSte committed Oct 3, 2022
1 parent b0db3b9 commit 7c9df4b
Show file tree
Hide file tree
Showing 7 changed files with 176 additions and 0 deletions.
2 changes: 2 additions & 0 deletions modules/authentication/src/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import twitchConfig from './twitch.config';
import slackConfig from './slack.config';
import tokenConfig from './token.config';
import localConfig from './local.config';
import magicLinkConfig from './magicLink.config';

const AppConfigSchema = {
...DefaultConfig,
Expand All @@ -21,6 +22,7 @@ const AppConfigSchema = {
...slackConfig,
...tokenConfig,
...localConfig,
...magicLinkConfig,
};
const config = convict(AppConfigSchema);
const configProperties = config.getProperties();
Expand Down
13 changes: 13 additions & 0 deletions modules/authentication/src/config/magicLink.config.ts
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,
},
},
};
1 change: 1 addition & 0 deletions modules/authentication/src/constants/TokenType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ export enum TokenType {
VERIFY_PHONE_NUMBER_TOKEN = 'VERIFY_PHONE_NUMBER_TOKEN',
LOGIN_WITH_PHONE_NUMBER_TOKEN = 'LOGIN_WITH_PHONE_NUMBER_TOKEN',
REGISTER_WITH_PHONE_NUMBER_TOKEN = 'REGISTER_WITH_PHONE_NUMBER_TOKEN',
MAGIC_LINK = 'MAGIC_LINK',
}
143 changes: 143 additions & 0 deletions modules/authentication/src/handlers/magicLink.ts
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);
}
}
10 changes: 10 additions & 0 deletions modules/authentication/src/routes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { OAuth2Settings } from '../handlers/oauth2/interfaces/OAuth2Settings';
import { TwoFa } from '../handlers/twoFa';
import { TokenProvider } from '../handlers/tokenProvider';
import authMiddleware from './middleware';
import { MagicLinkHandlers } from '../handlers/magicLink';

type OAuthHandler = typeof oauth2;

Expand All @@ -26,6 +27,7 @@ export class AuthenticationRoutes {
private readonly phoneHandlers: PhoneHandlers;
private readonly _routingManager: RoutingManager;
private readonly twoFaHandlers: TwoFa;
private readonly magicLinkHandlers: MagicLinkHandlers;

constructor(readonly server: GrpcServer, private readonly grpcSdk: ConduitGrpcSdk) {
this._routingManager = new RoutingManager(this.grpcSdk.router!, server);
Expand All @@ -34,6 +36,7 @@ export class AuthenticationRoutes {
this.phoneHandlers = new PhoneHandlers(grpcSdk);
this.localHandlers = new LocalHandlers(this.grpcSdk);
this.twoFaHandlers = new TwoFa(this.grpcSdk);
this.magicLinkHandlers = new MagicLinkHandlers(this.grpcSdk);
// initialize SDK
TokenProvider.getInstance(grpcSdk);
}
Expand All @@ -51,6 +54,13 @@ export class AuthenticationRoutes {
await this.phoneHandlers.declareRoutes(this._routingManager);
}

const magicLinkActive = await this.magicLinkHandlers
.validate()
.catch(e => (errorMessage = e));
if (magicLinkActive && !errorMessage) {
await this.magicLinkHandlers.declareRoutes(this._routingManager);
}

let authActive = await this.localHandlers.validate().catch(e => (errorMessage = e));
if (!errorMessage && authActive) {
await this.localHandlers.declareRoutes(this._routingManager);
Expand Down
6 changes: 6 additions & 0 deletions modules/authentication/src/templates/MagicLink.ts
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'],
};
1 change: 1 addition & 0 deletions modules/authentication/src/templates/index.ts
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';

0 comments on commit 7c9df4b

Please sign in to comment.