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

Refactored password and totp login challenges into a strategy pattern. #560

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/login/challenge/abstract.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { LoginChallengeContext } from '../types.js';

export abstract class AbstractLoginChallenge {

abstract challenge(loginContext: LoginChallengeContext): Promise<void>;

}
33 changes: 33 additions & 0 deletions src/login/challenge/password.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { AbstractLoginChallenge } from './abstract.js';
import { LoginChallengeContext } from '../types.js';
import { A12nLoginChallengeError } from '../error.js';
import * as services from '../../services.js';

export class LoginChallengePassword extends AbstractLoginChallenge {

async challenge(loginContext: LoginChallengeContext): Promise<void> {

if (loginContext.parameters.password === undefined) {
throw new A12nLoginChallengeError(
loginContext.session,
'A username and password are required',
'username_or_password_required',
);

}

const { success, errorMessage } = await services.user.validateUserCredentials(loginContext.principal, loginContext.parameters.password, loginContext.log);
if (!success && errorMessage) {
throw new A12nLoginChallengeError(
loginContext.session,
errorMessage,
'username_or_password_invalid',
);
}

loginContext.session.authFactorsPassed.push('password');
loginContext.dirty = true;

}

}
55 changes: 55 additions & 0 deletions src/login/challenge/totp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { AbstractLoginChallenge } from './abstract.js';
import { LoginChallengeContext } from '../types.js';
import { A12nLoginChallengeError } from '../error.js';
import * as services from '../../services.js';
import { InvalidGrant } from '../../oauth2/errors.js';
import { getSetting } from '../../server-settings.js';
export class LoginChallengeTotp extends AbstractLoginChallenge {

async challenge(loginContext: LoginChallengeContext): Promise<void> {

const serverTotpMode = getSetting('totp');
if (serverTotpMode === 'disabled') {
// Server-wide TOTP disabled.
loginContext.session.authFactorsPassed.push('totp');
loginContext.dirty = true;
return;
}
const hasTotp = await services.mfaTotp.hasTotp(loginContext.principal);
if (!hasTotp) {
// Does this server require TOTP
if (serverTotpMode === 'required') {
throw new InvalidGrant('This server is configured to require TOTP, and this user does not have TOTP set up. Logging in is not possible for this user in its current state. Contact an administrator');
}
// User didn't have TOTP so we just pass them
loginContext.session.authFactorsPassed.push('totp');
loginContext.dirty = true;
return;
}
if (!loginContext.parameters.totp_code) {
// No TOTP code was provided
throw new A12nLoginChallengeError(
loginContext.session,
'Please provide a TOTP code from the user\'s authenticator app.',
'totp_required',
);
}
if (!await services.mfaTotp.validateTotp(loginContext.principal, loginContext.parameters.totp_code)) {
loginContext.log('totp-failed');
// TOTP code was incorrect
throw new A12nLoginChallengeError(
loginContext.session,
'Incorrect TOTP code. Make sure your system clock is set to the correct time and try again',
'totp_invalid',
);
} else {
loginContext.log('totp-success');
};

// TOTP check successful!
loginContext.session.authFactorsPassed.push('totp');
loginContext.dirty = true;

}

}
45 changes: 45 additions & 0 deletions src/login/error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { OAuth2Error } from '../oauth2/errors.js';
import { LoginSession } from './types.js';

type ChallengeErrorCode =
// Account is not activated
| 'account_not_active'
// The principal associated with the credentials are not a user
| 'not_a_user'
// Username or password was wrong
| 'username_or_password_invalid'
// Username or password must be provided
| 'username_or_password_required'
// User must enter a TOTP code to continue
| 'totp_required'
// The TOTP code that was provided is invalid.
| 'totp_invalid'
// The email address used to log in was not verified
| 'email_not_verified';

export class A12nLoginChallengeError extends OAuth2Error {

httpStatus = 400;
errorCode: ChallengeErrorCode;
session: LoginSession;

constructor(session: LoginSession, message: string, errorCode: ChallengeErrorCode) {

super(message);
this.errorCode = errorCode;
this.session = session;

}

serializeErrorBody() {

return {
error: this.errorCode,
error_description: this.message,
auth_session: this.session.authSession,
expires_at: this.session.expiresAt,
};

}

}
Loading
Loading