Recipes - Third party authentication
You can extend your UserService to use third-party authentication services like Google Identity or Facebook Login.
Account Creation / Login
First, add a thirdPartyAuth
field and a thirdPartyUniqueId
field in your user entity.
@Entity()
export class User implements CrudUser {
//...
@Property()
thirdPartyAuth: string;
@Property()
thirdPartyUniqueId: string;
//...
Then, generate a command to log/register with your third-party authentication provider.
Let's change the DTO to accept the id_token
used in your third-party identification process.
import { IsString, IsIn } from 'class-validator';
export class LinkGoogleAccountDto {
@IsString()
id_token: string;
@Max(60*60*24*7) // 7 days
expiresInSec: number;
}
Then, let's allow guests to use the link_google_account
command in the security.
const getCmdSecurity = (link_google_account, profile): CmdSecurity => {
return {
dto: LinkGoogleAccountDto,
rolesRights: {
guest: {
async defineCMDAbility(can, cannot, ctx) {
// Define abilities for guest
can(link_google_account, profile);
}
}
},
}
}
You can now use your authentication provider's SDK to validate the received token and obtain the user's unique ID.
import { UnauthorizedException } from '@nestjs/common';
import { CrudErrors } from '@eicrud/shared/CrudErrors';
import { User } from "../../user.entity";
export async function link_google_account(this: UserService, dto: LinkGoogleAccountDto, ctx: CrudContext, inheritance?: any ){
const { email, valid, uuid } = await googleSDK(dto.id_token); //pseudocode
if(!valid){
throw new UnauthorizedException(CrudErrors.INVALID_CREDENTIALS.str());
}
const entity = { thirdPartyUniqueId: uuid };
let res = await this.$findOne(entity, ctx);
if(!res){ // if user doesn't exist, we create a new account
const user = new User();
user.email = email;
user.password = Math.random().toString(36);
user.thirdPartyUniqueId = uuid;
user.thirdPartyAuth = 'google';
res = await this.$create(user, ctx);
}
return {
userId: res[this.crudConfig.id_field],
accessToken: dto.logMeIn ? await this.authService.signTokenForUser(ctx, res, dto.expiresInSec) : undefined,
};
}
Finally, let's call the command from your front end.
const dto = {
id_token: 'f05415b13acb9590f70df862765c655f5a7a019e',
expiresInSec: 60*60*24*7
};
const { userId, accessToken } = await userClient.cmd('link_google_account', dto);
userClient.setJwt(accessToken, 60*60*24*7); // expires in 7 days
Note
The client->setJwt
method stores the provided token so that it can be used with every client request. It has no effect if you configured your JWT storage to cookie.
Disable regular login
You can disable regular login for users registered with third-party authentication. To do so simply extend the $authUser
method of your UserService.
import { UnauthorizedException } from '@nestjs/common';
import { ILoginDto } from '@eicrud/shared/interfaces';
// ...
override async $authUser(
ctx: CrudContext,
user: CrudUser,
dto: ILoginDto,
) {
if(user.thirdPartyAuth){
throw new UnauthorizedException('Must use third party login command.');
}
return super.$authUser(ctx, user, dto);
}
// ...
Renew JWT
When calling checkJwt, the validity of an issued token is automatically extended. You might want to change that behavior for users authenticated with third-parties.
You can disable the renewal by setting noTokenRefresh
when creating your user.
// ...
user.thirdPartyAuth = 'google';
user.noTokenRefresh = true;
// ...
Alternatively, you can extend the $renewJwt
method of your UserService to handle that behavior manually.
// ...
override async $renewJwt(ctx: CrudContext) {
const user = ctx.user;
if(user.thirdPartyAuth){
return { accessToken: null, refreshTokenSec: null};
}
return super.$renewJwt(ctx);
}
// ...
Note
The method signTokenForUser
let you store additional data in your JWT (accessible with CrudContext->jwtPayload
). You can use this to store the id_token
and check your third-party authentication provider before renewing a JWT.
return {
userId: res[this.crudConfig.id_field],
accessToken: dto.logMeIn ? await service.authService.signTokenForUser(ctx, res, dto.expiresInSec, { id_token: dto.id_token}) : undefined,
};
// ...
if(user.thirdPartyAuth){
const id_token = ctx.jwtPayload.id_token;
const renewed = await googleSDKRefresh(id_token) //pseudocode
if(renewed){
return super.$renewJwt(ctx, {id_token: renewed});
}
return { accessToken: null, refreshTokenSec: null};
}
signTokenForUser
automatically adds the JWT cookie to your CrudContext->setCookies
. In that case, you don't need to return the token in your response's payload.