feat: working interaction login

This commit is contained in:
Kakious 2024-10-15 12:14:16 -04:00
parent e3ba6bf1fd
commit ea98ed7976
7 changed files with 120 additions and 71 deletions

View file

@ -19,31 +19,17 @@ export class AuthController {
public async postLogin( public async postLogin(
@Body() body: LoginUserDto, @Body() body: LoginUserDto,
@Res({ passthrough: true }) res: Response, @Res({ passthrough: true }) res: Response,
@Req() request: Request, @Req() req: Request,
): Promise<any> { ): Promise<any> {
const sessionData = await this.authService.login(body.username, body.password); const userId = await this.authService.login(body.username, body.password);
sessionData.cookiesForms.forEach((cookie) => { const interaction = await this.authService.checkInteractionStatus(req, res);
res.cookie(cookie.name, cookie.value, cookie.options);
});
// if loginRedirect cookie is set, redirect to that page. if (!interaction) {
return await this.authService.createSession(res, userId);
console.log(request.cookies);
if (request.cookies['interactionId']) {
console.log('interactionRedirect');
return {
status: 'interactionRedirect',
interactionId: request.cookies['interactionId'],
};
} }
console.log('Logged in successfully'); return await this.authService.loginExistingInteraction(req, res, userId);
return {
status: 'success',
message: 'Logged in successfully',
sessionId: sessionData.sessionId,
};
} }
@Post('register') @Post('register')

View file

@ -1,6 +1,5 @@
import { import {
BadRequestException, BadRequestException,
Body,
Controller, Controller,
Get, Get,
InternalServerErrorException, InternalServerErrorException,
@ -19,7 +18,6 @@ import { InteractionService } from '../oidc/service/interaction.service';
import { OidcService } from '../oidc/service/core.service'; import { OidcService } from '../oidc/service/core.service';
import { InteractionParams, InteractionSession } from '../oidc/types/interaction.type'; import { InteractionParams, InteractionSession } from '../oidc/types/interaction.type';
import { User as UserObject } from 'src/database/models/user.model'; import { User as UserObject } from 'src/database/models/user.model';
import { LoginUserDto } from '../dto/loginUser.dto';
@Controller('interaction') @Controller('interaction')
@ApiExcludeController() @ApiExcludeController()
@ -58,22 +56,15 @@ export class InteractionController {
const cookies = req.cookies; const cookies = req.cookies;
res.cookie('interactionId', uid, { httpOnly: true, path: '/auth/login', secure: true }); // Log in console when the packet expires
res.cookie('_interaction', cookies['_interaction'], {
httpOnly: true,
path: '/auth/login',
secure: true,
expires: new Date(Date.now() + 900000),
});
res.cookie('_interaction.sig', cookies['_interaction.sig'], { console.log(cookies);
httpOnly: true,
path: '/auth/login',
secure: true,
expires: new Date(Date.now() + 900000),
});
return res.redirect('/auth/login'); return await this.interactionService.interactionRedirectToLoginPage(
res,
uid,
cookies['_interaction.sig'],
);
} }
case 'consent': { case 'consent': {
@ -94,6 +85,7 @@ export class InteractionController {
return res.render('interaction/consent', { return res.render('interaction/consent', {
client: { client: {
clientName: details.params.client_id, clientName: details.params.client_id,
//TODO: Actually have this work
clientLogo: 'https://via.placeholder.com/150', clientLogo: 'https://via.placeholder.com/150',
}, },
uid, uid,
@ -117,20 +109,4 @@ export class InteractionController {
async denyInteraction(@Req() req: Request, @Res() res: Response) { async denyInteraction(@Req() req: Request, @Res() res: Response) {
return this.interactionService.abort(req, res); return this.interactionService.abort(req, res);
} }
@Post(':id/login')
async loginInteraction(@Body() login: LoginUserDto, @Req() req: Request, @Res() res: Response) {
const userId = await this.authService.validateLogin(login.username, login.password);
if (!userId) {
throw new BadRequestException('Invalid login');
}
const redirectUrl = await this.interactionService.login(req, res, userId, true);
res.json({
redirectUrl,
status: 'interactionRedirect',
});
}
} }

View file

@ -13,3 +13,11 @@ export const GRANT_LIFE = 60 * 60 * 24 * 30; // 1 month
export const INTERACTION_LIFE = 1 * 60 * 60; // 1 hour export const INTERACTION_LIFE = 1 * 60 * 60; // 1 hour
export const PUSHED_AUTH_REQ_LIFE = 15 * 60; // 15 minutes export const PUSHED_AUTH_REQ_LIFE = 15 * 60; // 15 minutes
export const CLIENT_CREDENTIALS_TOKEN_LIFE = 7 * 24 * 60 * 60; // 7 days export const CLIENT_CREDENTIALS_TOKEN_LIFE = 7 * 24 * 60 * 60; // 7 days
//====Helper functions====
/**
* Creates a new date for the cookie expiry based on the interaction life
* @returns Date
*/
export const createInteractionExpiry = () => new Date(Date.now() + INTERACTION_LIFE * 1000);

View file

@ -10,6 +10,7 @@ import { Span } from 'nestjs-otel';
import { Interaction } from '../types/interaction.type'; import { Interaction } from '../types/interaction.type';
import { OidcService } from './core.service'; import { OidcService } from './core.service';
import { createInteractionExpiry } from '../oidc.const';
@Injectable() @Injectable()
export class InteractionService { export class InteractionService {
@ -33,6 +34,9 @@ export class InteractionService {
} }
} }
// TODO: Does a direct check inside of redis and returns a true/false depending if the session exists.
public async checkExisting() {}
/** /**
* Get a user from the interaction * Get a user from the interaction
* @param req The request * @param req The request
@ -215,4 +219,37 @@ export class InteractionService {
}); });
} }
} }
/**
*
*/
async interactionRedirectToLoginPage(
res: Response,
interactionId: string,
interactionSig: string,
) {
res.cookie('interactionId', interactionId, {
httpOnly: true,
path: '/auth/login',
secure: true,
expires: createInteractionExpiry(),
});
res.cookie('_interaction', interactionId, {
httpOnly: true,
path: '/auth/login',
secure: true,
expires: createInteractionExpiry(),
});
res.cookie('_interaction.sig', interactionSig, {
httpOnly: true,
path: '/auth/login',
secure: true,
expires: createInteractionExpiry(),
});
res.redirect('/auth/login');
}
} }

View file

@ -1,4 +1,5 @@
import { BadRequestException, Injectable, UnauthorizedException } from '@nestjs/common'; import { BadRequestException, Injectable, UnauthorizedException } from '@nestjs/common';
import { Response, Request } from 'express';
import { UserService } from '../../user/service/user.service'; import { UserService } from '../../user/service/user.service';
import { RedisService } from '../../redis/service/redis.service'; import { RedisService } from '../../redis/service/redis.service';
@ -11,6 +12,8 @@ import {
PASSWORD_RESET_EXPIRATION, PASSWORD_RESET_EXPIRATION,
} from '../auth.const'; } from '../auth.const';
import { OidcService } from '../oidc/service/core.service'; import { OidcService } from '../oidc/service/core.service';
import { InteractionService } from '../oidc/service/interaction.service';
import { Interaction } from '../oidc/types/interaction.type';
@Injectable() @Injectable()
export class AuthService { export class AuthService {
@ -18,6 +21,7 @@ export class AuthService {
private readonly userService: UserService, private readonly userService: UserService,
private readonly redisService: RedisService, private readonly redisService: RedisService,
private readonly oidcService: OidcService, private readonly oidcService: OidcService,
private readonly interactionService: InteractionService,
private readonly mailService: MailService, private readonly mailService: MailService,
) {} ) {}
@ -51,41 +55,75 @@ export class AuthService {
} }
/** /**
* Validate a user's login credentials and return the session cookies * Fetches an Interaction from the Interaction Service. Will return null if one is not found.
* @param username The username or email address of the user * @param req The Request Object
* @param password The password of the user * @param res The Response Object
* @returns The session cookies * @returns Interaction | null
*/ */
public async login(username: string, password: string): Promise<any> { public async checkInteractionStatus(req: Request, res: Response): Promise<Interaction | null> {
const user = await this.userService.authenticate(username, password); try {
return await this.interactionService.get(req, res);
if (!user) { } catch (e) {
throw new BadRequestException('Invalid credentials'); return null;
} }
if (!user.emailVerified) {
throw new UnauthorizedException('Email not verified');
}
return this.oidcService.createSession(user.id);
} }
/** /**
* Validate a user's login credential and return the user id * Validate a user's login credentials
* @param username The username or email address of the user * @param username The username or email address of the user
* @param password The password of the user * @param password The password of the user
* @returns The user id * @returns The user id
*/ */
public async validateLogin(username: string, password: string): Promise<string> { public async login(username: string, password: string): Promise<string> {
const user = await this.userService.authenticate(username, password); const user = await this.userService.authenticate(username, password);
if (!user) { if (!user) {
throw new BadRequestException('Invalid credentials'); throw new UnauthorizedException('Invalid credentials');
} }
return user.id; return user.id;
} }
//TODO: Make response DTOs
/**
* Create a session for a user and set the session copokies.
* @param response The Response object
* @param userId The User's ID
* @returns
*/
public async createSession(res: Response, userId: string) {
const sessionData = await this.oidcService.createSession(userId);
sessionData.cookiesForms.forEach((cookie) => {
res.cookie(cookie.name, cookie.value, {
...cookie.options,
sameSite: cookie.options.sameSite as 'strict' | 'lax' | 'none' | undefined,
});
});
return {
status: 'success',
message: 'Login Successful',
sessionId: sessionData.sessionId,
};
}
/**
* This will login an existing interaction and instruct the browser to redirect to the interaction flow
* @param interaction The interaction object
* @param userId The User's ID
* @return
*/
public async loginExistingInteraction(req: Request, res: Response, userId: string) {
const redirectUrl = await this.interactionService.login(req, res, userId, true);
return {
status: 'redirect',
message: 'Login Successful, Redirect to session page',
redirectUrl,
};
}
// == Password Reset Logic == // // == Password Reset Logic == //
/** /**

View file

@ -0,0 +1,4 @@
import { Injectable } from '@nestjs/common';
@Injectable()
export class S3Service {}

View file

@ -196,7 +196,7 @@ document.getElementById('loginForm').addEventListener('submit', async function(e
} }
} else { } else {
const responseData = await response.json(); const responseData = await response.json();
if (responseData.status === 'interactionRedirect' && responseData.redirectUrl) { if (responseData.status === 'redirect' && responseData.redirectUrl) {
window.location.href = responseData.redirectUrl; window.location.href = responseData.redirectUrl;
} else { } else {
window.location.href = '/auth/auth-test'; window.location.href = '/auth/auth-test';