Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(authentication): configurable magicLink dispatch URI, magic token consumption endpoint #722

Merged
merged 7 commits into from
Oct 10, 2023
6 changes: 6 additions & 0 deletions modules/authentication/src/config/magicLink.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,11 @@ export default {
default: '',
optional: true,
},
link_uri: {
doc: 'When specified, this setting overrides the default magic link URI, allowing you to customize the destination where the magic token is sent. Used for manual consumption and token exchange.',
format: 'String',
default: '',
optional: true,
},
},
};
96 changes: 66 additions & 30 deletions modules/authentication/src/handlers/magicLink.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { isNil } from 'lodash';
import { isEmpty, isNil } from 'lodash';
import { TokenType } from '../constants';
import { v4 as uuid } from 'uuid';
import ConduitGrpcSdk, {
Expand All @@ -15,7 +15,7 @@ import {
ConfigController,
RoutingManager,
} from '@conduitplatform/module-tools';
import { Token, User } from '../models';
import { Client, Token, User } from '../models';
import { status } from '@grpc/grpc-js';
import { IAuthenticationStrategy } from '../interfaces';
import { TokenProvider } from './tokenProvider';
Expand Down Expand Up @@ -72,25 +72,39 @@ export class MagicLinkHandlers implements IAuthenticationStrategy {
urlParams: {
verificationToken: ConduitString.Required,
},
queryParams: config.customRedirectUris
? { redirectUri: ConduitString.Optional }
: {},
},
new ConduitRouteReturnDefinition('VerifyMagicLinkLoginResponse', {
accessToken: ConduitString.Optional,
refreshToken: ConduitString.Optional,
}),
this.verifyLogin.bind(this),
);
if (!isEmpty(config.magic_link.link_uri)) {
routingManager.route(
{
path: '/magic-link/:magicToken',
action: ConduitRouteActions.POST,
description: 'Exchange magic token for login tokens.',
urlParams: {
magicToken: ConduitString.Required,
},
},
new ConduitRouteReturnDefinition('MagicLinkExchangeResponse', {
accessToken: ConduitString.Optional,
refreshToken: ConduitString.Optional,
}),
this.exchangeMagicToken.bind(this),
);
}
}

async sendMagicLink(call: ParsedRouterRequest): Promise<UnparsedRouterResponse> {
const email = call.request.params.email.toLowerCase();
const config = ConfigController.getInstance().config;
const redirectUri =
config.customRedirectUris && !isNil(call.request.bodyParams.redirectUri)
config.customRedirectUris && !isEmpty(call.request.bodyParams.redirectUri)
? call.request.bodyParams.redirectUri
: config.magic_link.redirect_uri;
: undefined;
const { clientId } = call.request.context;
const user: User | null = await User.getInstance().findOne({ email });
if (isNil(user)) throw new GrpcError(status.NOT_FOUND, 'User not found');
Expand All @@ -100,24 +114,51 @@ export class MagicLinkHandlers implements IAuthenticationStrategy {
user: user._id,
data: {
clientId,
...(redirectUri !== undefined ? { redirectUri } : {}),
},
token: uuid(),
});

await this.sendMagicLinkMail(user, token, redirectUri);
await this.sendMagicLinkMail(user, token);
return 'token sent';
}

async verifyLogin(call: ParsedRouterRequest): Promise<UnparsedRouterResponse> {
const { verificationToken } = call.request.urlParams;
const config = ConfigController.getInstance().config;
const { user, data } = await this.redeemMagicToken(verificationToken);
const redirectUri =
config.customRedirectUris && !isNil(call.request.queryParams.redirectUri)
? call.request.queryParams.redirectUri
: config.magic_link.redirect_uri;
config.customRedirectUris && data.redirectUri
? data.redirectUri
: config.magic_link.redirect_uri.replace(/\/$/, '');
return TokenProvider.getInstance().provideUserTokens(
{
user,
clientId: data.clientId,
config,
},
redirectUri,
);
}

private async exchangeMagicToken(
call: ParsedRouterRequest,
): Promise<UnparsedRouterResponse> {
const { magicToken } = call.request.urlParams;
const { user, data } = await this.redeemMagicToken(magicToken);
return TokenProvider.getInstance().provideUserTokens({
user,
clientId: data.clientId,
config: ConfigController.getInstance().config,
});
}

private async redeemMagicToken(
magicToken: string,
): Promise<{ user: User; data: Token['data'] & { clientId: Client['clientId'] } }> {
const token: Token | null = await Token.getInstance().findOne({
tokenType: TokenType.MAGIC_LINK,
token: verificationToken,
token: magicToken,
});
if (isNil(token)) {
throw new GrpcError(status.NOT_FOUND, 'Magic link token does not exist');
Expand All @@ -126,32 +167,27 @@ export class MagicLinkHandlers implements IAuthenticationStrategy {
_id: token.user! as string,
});
if (isNil(user)) throw new GrpcError(status.NOT_FOUND, 'User not found');

await Token.getInstance()
.deleteMany({ user: token.user, tokenType: TokenType.MAGIC_LINK })
.catch(e => {
ConduitGrpcSdk.Logger.error(e);
});

return TokenProvider.getInstance().provideUserTokens(
{
user,
clientId: token.data.clientId,
config,
},
redirectUri,
);
return {
user,
data: token.data,
};
}

private async sendMagicLinkMail(user: User, token: Token, redirectUri?: string) {
private async sendMagicLinkMail(user: User, token: Token) {
const linkUri = ConfigController.getInstance().config.magic_link.link_uri?.replace(
/\/$/,
'',
);
const serverConfig = await this.grpcSdk.config.get('router');
const url = serverConfig.hostUrl;

const result = { token, hostUrl: url };
let link = `${result.hostUrl}/hook/authentication/magic-link/${result.token.token}`;
if (redirectUri) {
link = link.concat(`?redirectUri=${redirectUri}`);
}
const baseUrl = !isEmpty(linkUri)
? linkUri
: `${serverConfig.hostUrl.replace(/\/$/, '')}/hook/authentication/magic-link`;
const link = `${baseUrl}/${token.token}`;
await this.emailModule.sendEmail('MagicLink', {
email: user.email,
sender: 'no-reply',
Expand Down
2 changes: 1 addition & 1 deletion modules/authentication/src/models/Client.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { ConduitActiveSchema } from '@conduitplatform/module-tools';
export class Client extends ConduitActiveSchema<Client> {
private static _instance: Client;
_id!: string;
clientId!: string;
clientId!: string | 'anonymous-client';
clientSecret!: string;
alias?: string;
notes?: string;
Expand Down
Loading