I have an API backend implemented using Nest, and the app frontend is implemented using Angular. It implements authentication using Passport.
The Angular app runs at http://localhost:2080, whereas the Nestjs app run at http://localhost:2180 and is mounted at http://localhost:2080/api using HTTP proxy middleware.
I have implemented Local, Magic Login, Google and Facebook strategies for authentication.
When a user authenticates using Local, Magic Login, Google, or Facebook strategies, on the frontend, they are redirected to the /
path.
There is a /app/manage-profile
path, which allows a logged in user to connect or disconnect their Google or Facebook account to their local account.
I have the following methods in my auth
controller.
// this corresponds to /api/auth/login/google
@UseGuards(AuthGoogleAuthGuard)
@Get('google')
async loginWithGoogle(): Promise<void> {
// NOTE : do nothing
}
// this corresponds to /api/auth/connect/google
@UseGuards(AuthGoogleAuthGuard)
@Get('google')
async connectGoogle(): Promise<void> {
// NOTE : do nothing
}
// this corresponds to /api/auth/accept/google
@UseGuards(AuthGoogleAuthGuard)
@Get('google')
async acceptGoogle(@Req() req: Request, @Res() res: Response): Promise<void> {
await this.tokenService.invalidateToken(extractSession(req));
const result: Result<Token> = await this.tokenService.generateToken(req.user as User);
if (result.success) {
const redirectURL: URL = new URL('/app/accept/google', AUTH_CONSTANTS.Strategies.Google.redirectURL);
redirectURL.searchParams.set('token', result.data.token);
res.redirect(redirectURL.toString());
} else {
const redirectURL: URL = new URL('/app/login', AUTH_CONSTANTS.Strategies.Google.redirectURL);
res.redirect(redirectURL.toString());
}
}
// this corresponds to /api/auth/disconnect/google
@UseGuards(AuthJwtAuthGuard)
@Get('google')
async disconnectGoogle(@Req() req: Request): Promise<Result<Token>> {
const user: User = await this.userService.ensureUserNotWithProvider(req.user as User, ProviderType.GOOGLE);
await this.tokenService.invalidateToken(extractSession(req));
return await this.tokenService.generateToken(user);
}
I am struggling to redirect the user to /
when they are logging in, but to /app/manage-profile
when they are connecting their account. This is because when the user is redirected back to my app from Google after authentication, they are redirected back to /api/accept/google
. In there, I can't differentiate whether they initiated via Login or Connect.
Can someone point me in the right direction?
Passport
allows a state to be sent to the OAuth provider, which is then echoed back to the API backend sending the uset to the OAuth provider for authentication.
To return the user back to a particular route in the app frontend, an item of state should be set to identify the location.
Then in the callback, that state should be interpreted and the location in the app frontend set to the desired on.
Suppose we are using Google
as the OAuth provider.
Step 1: user initiates "login" or "connect" on the app frontend
The frontend will redirect the user to /api/login/google?path=/
or /api/connect/google?path=/app/manage-profile
, as the case may be.
Step 2: user is redirected by the API backend to Google
The backend will extract the value of path
query parameter from the request, and set the value of state
to the base64-encoded value of the path
query parameter.
NB: some other memoization method can be used here too, but for my purposes this was enough
Step 3: user is received back by the API backend from Google
The /api/accept/google
endpoint will be called by Google after the user successfully authenticates on Google. The request will include the state
we set in Step 2.
The state
will be "interpreted" -- in our case, just base-64 decoded -- the value of path
extracted and the user will be redirected to the app frontend using the value of path
.
Important:
The way to pass state in Nest during OAuth authorization using Passport is to implement the getAuthenticationOptions
in the AuthGuard implementation for the Passport strategy.
For example,
import { ExecutionContext, Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class AuthGoogleAuthGuard extends AuthGuard('google') {
constructor(
private configService: ConfigService
) {
super({
accessType: 'offline'
});
}
getAuthenticateOptions(context: ExecutionContext) {
// get the path out of the query parameters
const { path } = context.switchToHttp().getRequest().query;
// create a JSON object with all of the state that needs to be persisted during the OAuth flow
const json: string = JSON.stringify({ path });
// stringify the state, and base64-encode it
// alternatively, this state object can be persisted to a cache, and the cache key sent as the state
const state: string = Buffer
.from(json, 'utf-8')
.toString('base64');
// set the state; this will be returned back in the callback from the OAuth provider
return {
state
};
}
}
Then in the accept
callback...
@UseGuards(AuthGoogleAuthGuard)
@Get('accept/google')
async acceptGoogle(@Req() req: Request, @Res() res: Response, @Query('state') state: string): Promise<void> {
// decode the state query parameter
const json: string = Buffer
.from(state, 'base64')
.toString('utf-8');
// if we had used a cache to store the state, here we can pull the state out from the cache using the key received in the state
// parse it back into a JSON object
const { path }: { path: string; } = JSON.parse(json) as { path: string; };
await this.tokenService.invalidateToken(extractSession(req));
const result: Result<Token> = await this.tokenService.generateToken(req.user as User);
if (result.success) {
const redirectURL: URL = new URL('/app/accept/google', AUTH_CONSTANTS.Strategies.Google.redirectURL);
redirectURL.searchParams.set('token', result.data.token);
// include the path received in the state in the URL redirecting user to the app frontend
redirectURL.searchParams.set('path', path);
res.redirect(redirectURL.toString());
} else {
const redirectURL: URL = new URL('/app/login', AUTH_CONSTANTS.Strategies.Google.redirectURL);
res.redirect(redirectURL.toString());
}
}