mirror of
https://github.com/0rangebananaspy/authelia.git
synced 2024-09-14 22:47:21 +07:00
Use issuer and label when generating otpauthURL for TOTP
Issuer is customizable in configuration so that a company can set its own name or website. If not provided, default value is 'authelia.com'. The username is used as label.
This commit is contained in:
parent
87056c14e2
commit
54854bacb1
|
@ -1,14 +1,6 @@
|
|||
import { TOTPSecret } from "../../../../types/TOTPSecret";
|
||||
|
||||
export interface GenerateSecretOptions {
|
||||
length?: number;
|
||||
symbols?: boolean;
|
||||
otpauth_url?: boolean;
|
||||
name?: string;
|
||||
issuer?: string;
|
||||
}
|
||||
|
||||
export interface ITotpHandler {
|
||||
generate(options?: GenerateSecretOptions): TOTPSecret;
|
||||
generate(label: string, issuer: string): TOTPSecret;
|
||||
validate(token: string, secret: string): boolean;
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
import { ITotpHandler, GenerateSecretOptions } from "./ITotpHandler";
|
||||
import { ITotpHandler } from "./ITotpHandler";
|
||||
import { TOTPSecret } from "../../../../types/TOTPSecret";
|
||||
import Speakeasy = require("speakeasy");
|
||||
|
||||
|
@ -12,8 +12,17 @@ export class TotpHandler implements ITotpHandler {
|
|||
this.speakeasy = speakeasy;
|
||||
}
|
||||
|
||||
generate(options?: GenerateSecretOptions): TOTPSecret {
|
||||
return this.speakeasy.generateSecret(options);
|
||||
generate(label: string, issuer: string): TOTPSecret {
|
||||
const secret = this.speakeasy.generateSecret({
|
||||
otpauth_url: false
|
||||
});
|
||||
|
||||
secret.otpauth_url = this.speakeasy.otpauthURL({
|
||||
secret: secret.ascii,
|
||||
label: label,
|
||||
issuer: issuer
|
||||
});
|
||||
return secret;
|
||||
}
|
||||
|
||||
validate(token: string, secret: string): boolean {
|
||||
|
@ -22,6 +31,6 @@ export class TotpHandler implements ITotpHandler {
|
|||
encoding: TOTP_ENCODING,
|
||||
token: token,
|
||||
window: WINDOW
|
||||
} as any);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -35,7 +35,7 @@ type UserName = string;
|
|||
type GroupName = string;
|
||||
type DomainPattern = string;
|
||||
|
||||
export type ACLPolicy = 'deny' | 'allow';
|
||||
export type ACLPolicy = 'deny' | 'allow';
|
||||
|
||||
export type ACLRule = {
|
||||
domain: string;
|
||||
|
@ -121,6 +121,10 @@ export interface AuthenticationMethodsConfiguration {
|
|||
per_subdomain_methods?: AuthenticationMethodPerSubdomain;
|
||||
}
|
||||
|
||||
export interface TOTPConfiguration {
|
||||
issuer: string;
|
||||
}
|
||||
|
||||
export interface UserConfiguration {
|
||||
port?: number;
|
||||
logs_level?: string;
|
||||
|
@ -132,6 +136,7 @@ export interface UserConfiguration {
|
|||
access_control?: ACLConfiguration;
|
||||
regulation: RegulationConfiguration;
|
||||
default_redirection_url?: string;
|
||||
totp?: TOTPConfiguration;
|
||||
}
|
||||
|
||||
export interface AppConfiguration {
|
||||
|
@ -145,4 +150,5 @@ export interface AppConfiguration {
|
|||
access_control?: ACLConfiguration;
|
||||
regulation: RegulationConfiguration;
|
||||
default_redirection_url?: string;
|
||||
totp: TOTPConfiguration;
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ import {
|
|||
} from "./Configuration";
|
||||
import Util = require("util");
|
||||
import { ACLAdapter } from "./adapters/ACLAdapter";
|
||||
import { TOTPAdapter } from "./adapters/TOTPAdapter";
|
||||
import { AuthenticationMethodsAdapter } from "./adapters/AuthenticationMethodsAdapter";
|
||||
import { Validator } from "./Validator";
|
||||
|
||||
|
@ -63,6 +64,7 @@ function adaptFromUserConfiguration(userConfiguration: UserConfiguration)
|
|||
const ldapConfiguration = adaptLdapConfiguration(userConfiguration.ldap);
|
||||
const authenticationMethods = AuthenticationMethodsAdapter
|
||||
.adapt(userConfiguration.authentication_methods);
|
||||
const totpConfiguration = TOTPAdapter.adapt(userConfiguration.totp);
|
||||
|
||||
return {
|
||||
port: port,
|
||||
|
@ -83,7 +85,8 @@ function adaptFromUserConfiguration(userConfiguration: UserConfiguration)
|
|||
access_control: ACLAdapter.adapt(userConfiguration.access_control),
|
||||
regulation: userConfiguration.regulation,
|
||||
authentication_methods: authenticationMethods,
|
||||
default_redirection_url: userConfiguration.default_redirection_url
|
||||
default_redirection_url: userConfiguration.default_redirection_url,
|
||||
totp: totpConfiguration
|
||||
};
|
||||
}
|
||||
|
||||
|
|
20
server/src/lib/configuration/adapters/TOTPAdapter.ts
Normal file
20
server/src/lib/configuration/adapters/TOTPAdapter.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
import { TOTPConfiguration } from "../Configuration";
|
||||
import { ObjectCloner } from "../../utils/ObjectCloner";
|
||||
|
||||
const DEFAULT_ISSUER = "authelia.com";
|
||||
|
||||
export class TOTPAdapter {
|
||||
static adapt(configuration: TOTPConfiguration): TOTPConfiguration {
|
||||
const newConfiguration = {
|
||||
issuer: DEFAULT_ISSUER
|
||||
};
|
||||
|
||||
if (!configuration)
|
||||
return newConfiguration;
|
||||
|
||||
if (configuration && configuration.issuer)
|
||||
newConfiguration.issuer = configuration.issuer;
|
||||
|
||||
return newConfiguration;
|
||||
}
|
||||
}
|
|
@ -16,19 +16,22 @@ import { IRequestLogger } from "../../../../logging/IRequestLogger";
|
|||
import { IUserDataStore } from "../../../../storage/IUserDataStore";
|
||||
import { ITotpHandler } from "../../../../authentication/totp/ITotpHandler";
|
||||
import { TOTPSecret } from "../../../../../../types/TOTPSecret";
|
||||
import { TOTPConfiguration } from "../../../../configuration/Configuration";
|
||||
|
||||
|
||||
export default class RegistrationHandler implements IdentityValidable {
|
||||
private logger: IRequestLogger;
|
||||
private userDataStore: IUserDataStore;
|
||||
private totp: ITotpHandler;
|
||||
private configuration: TOTPConfiguration;
|
||||
|
||||
constructor(logger: IRequestLogger,
|
||||
userDataStore: IUserDataStore,
|
||||
totp: ITotpHandler) {
|
||||
totp: ITotpHandler, configuration: TOTPConfiguration) {
|
||||
this.logger = logger;
|
||||
this.userDataStore = userDataStore;
|
||||
this.totp = totp;
|
||||
this.configuration = configuration;
|
||||
}
|
||||
|
||||
challenge(): string {
|
||||
|
@ -70,22 +73,24 @@ export default class RegistrationHandler implements IdentityValidable {
|
|||
return FirstFactorValidator.validate(req, this.logger);
|
||||
}
|
||||
|
||||
postValidationResponse(req: express.Request, res: express.Response): BluebirdPromise<void> {
|
||||
postValidationResponse(req: express.Request, res: express.Response)
|
||||
: BluebirdPromise<void> {
|
||||
const that = this;
|
||||
let secret: TOTPSecret;
|
||||
let userId: string;
|
||||
return new BluebirdPromise(function (resolve, reject) {
|
||||
const authSession = AuthenticationSessionHandler.get(req, that.logger);
|
||||
const challenge = authSession.identity_check.challenge;
|
||||
userId = authSession.identity_check.userid;
|
||||
userId = authSession.userid;
|
||||
|
||||
if (challenge != Constants.CHALLENGE || !userId) {
|
||||
if (authSession.identity_check.challenge != Constants.CHALLENGE
|
||||
|| !userId)
|
||||
return reject(new Error("Bad challenge."));
|
||||
}
|
||||
|
||||
resolve();
|
||||
})
|
||||
.then(function () {
|
||||
secret = that.totp.generate();
|
||||
secret = that.totp.generate(userId,
|
||||
that.configuration.issuer);
|
||||
that.logger.debug(req, "Save the TOTP secret in DB");
|
||||
return that.userDataStore.saveTOTPSecret(userId, secret);
|
||||
})
|
||||
|
|
|
@ -55,7 +55,7 @@ function setupTotp(app: Express.Application, vars: ServerVariables) {
|
|||
Endpoints.SECOND_FACTOR_TOTP_IDENTITY_START_GET,
|
||||
Endpoints.SECOND_FACTOR_TOTP_IDENTITY_FINISH_GET,
|
||||
new TOTPRegistrationIdentityHandler(vars.logger,
|
||||
vars.userDataStore, vars.totpHandler),
|
||||
vars.userDataStore, vars.totpHandler, vars.config.totp),
|
||||
vars);
|
||||
}
|
||||
|
||||
|
|
|
@ -16,6 +16,9 @@ describe("test session configuration builder", function () {
|
|||
users: {},
|
||||
groups: {}
|
||||
},
|
||||
totp: {
|
||||
issuer: "authelia.com"
|
||||
},
|
||||
ldap: {
|
||||
url: "ldap://ldap",
|
||||
user: "user",
|
||||
|
@ -90,6 +93,9 @@ describe("test session configuration builder", function () {
|
|||
users: {},
|
||||
groups: {}
|
||||
},
|
||||
totp: {
|
||||
issuer: "authelia.com"
|
||||
},
|
||||
ldap: {
|
||||
url: "ldap://ldap",
|
||||
user: "user",
|
||||
|
|
|
@ -35,6 +35,9 @@ export class ServerVariablesMockBuilder {
|
|||
authentication_methods: {
|
||||
default_method: "two_factor"
|
||||
},
|
||||
totp: {
|
||||
issuer: "authelia.com"
|
||||
},
|
||||
ldap: {
|
||||
url: "ldap://ldap",
|
||||
user: "user",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import Sinon = require("sinon");
|
||||
import BluebirdPromise = require("bluebird");
|
||||
import { ITotpHandler, GenerateSecretOptions } from "../../src/lib/authentication/totp/ITotpHandler";
|
||||
import { ITotpHandler } from "../../src/lib/authentication/totp/ITotpHandler";
|
||||
import { TOTPSecret } from "../../types/TOTPSecret";
|
||||
|
||||
export class TotpHandlerStub implements ITotpHandler {
|
||||
|
@ -12,8 +12,8 @@ export class TotpHandlerStub implements ITotpHandler {
|
|||
this.validateStub = Sinon.stub();
|
||||
}
|
||||
|
||||
generate(options?: GenerateSecretOptions): TOTPSecret {
|
||||
return this.generateStub(options);
|
||||
generate(label: string, issuer: string): TOTPSecret {
|
||||
return this.generateStub(label, issuer);
|
||||
}
|
||||
|
||||
validate(token: string, secret: string): boolean {
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
import Sinon = require("sinon");
|
||||
import winston = require("winston");
|
||||
import RegistrationHandler from "../../../../../src/lib/routes/secondfactor/totp/identity/RegistrationHandler";
|
||||
import { Identity } from "../../../../../types/Identity";
|
||||
import { UserDataStore } from "../../../../../src/lib/storage/UserDataStore";
|
||||
import assert = require("assert");
|
||||
import BluebirdPromise = require("bluebird");
|
||||
import ExpressMock = require("../../../../mocks/express");
|
||||
import { ServerVariablesMock, ServerVariablesMockBuilder } from "../../../../mocks/ServerVariablesMockBuilder";
|
||||
import { ServerVariablesMock, ServerVariablesMockBuilder }
|
||||
from "../../../../mocks/ServerVariablesMockBuilder";
|
||||
import { ServerVariables } from "../../../../../src/lib/ServerVariables";
|
||||
import Assert = require("assert");
|
||||
|
||||
describe("test totp register", function () {
|
||||
let req: ExpressMock.RequestMock;
|
||||
|
@ -26,7 +26,11 @@ describe("test totp register", function () {
|
|||
userid: "user",
|
||||
email: "user@example.com",
|
||||
first_factor: true,
|
||||
second_factor: false
|
||||
second_factor: false,
|
||||
identity_check: {
|
||||
userid: "user",
|
||||
challenge: "totp-register"
|
||||
}
|
||||
}
|
||||
};
|
||||
req.headers = {};
|
||||
|
@ -36,23 +40,29 @@ describe("test totp register", function () {
|
|||
inMemoryOnly: true
|
||||
};
|
||||
|
||||
mocks.userDataStore.saveU2FRegistrationStub.returns(BluebirdPromise.resolve({}));
|
||||
mocks.userDataStore.retrieveU2FRegistrationStub.returns(BluebirdPromise.resolve({}));
|
||||
mocks.userDataStore.produceIdentityValidationTokenStub.returns(BluebirdPromise.resolve({}));
|
||||
mocks.userDataStore.consumeIdentityValidationTokenStub.returns(BluebirdPromise.resolve({}));
|
||||
mocks.userDataStore.saveTOTPSecretStub.returns(BluebirdPromise.resolve({}));
|
||||
mocks.userDataStore.saveU2FRegistrationStub
|
||||
.returns(BluebirdPromise.resolve({}));
|
||||
mocks.userDataStore.retrieveU2FRegistrationStub
|
||||
.returns(BluebirdPromise.resolve({}));
|
||||
mocks.userDataStore.produceIdentityValidationTokenStub
|
||||
.returns(BluebirdPromise.resolve({}));
|
||||
mocks.userDataStore.consumeIdentityValidationTokenStub
|
||||
.returns(BluebirdPromise.resolve({}));
|
||||
mocks.userDataStore.saveTOTPSecretStub
|
||||
.returns(BluebirdPromise.resolve({}));
|
||||
|
||||
res = ExpressMock.ResponseMock();
|
||||
});
|
||||
|
||||
describe("test totp registration check", test_registration_check);
|
||||
|
||||
function test_registration_check() {
|
||||
describe("test totp registration pre validation", function () {
|
||||
it("should fail if first_factor has not been passed", function () {
|
||||
req.session.auth.first_factor = false;
|
||||
return new RegistrationHandler(vars.logger, vars.userDataStore, vars.totpHandler)
|
||||
return new RegistrationHandler(vars.logger, vars.userDataStore,
|
||||
vars.totpHandler, vars.config.totp)
|
||||
.preValidationInit(req as any)
|
||||
.then(function () { return BluebirdPromise.reject(new Error("It should fail")); })
|
||||
.then(function () {
|
||||
return BluebirdPromise.reject(new Error("It should fail"));
|
||||
})
|
||||
.catch(function (err: Error) {
|
||||
return BluebirdPromise.resolve();
|
||||
});
|
||||
|
@ -62,7 +72,8 @@ describe("test totp register", function () {
|
|||
req.session.auth.first_factor = false;
|
||||
req.session.auth.userid = undefined;
|
||||
|
||||
new RegistrationHandler(vars.logger, vars.userDataStore, vars.totpHandler)
|
||||
new RegistrationHandler(vars.logger, vars.userDataStore, vars.totpHandler,
|
||||
vars.config.totp)
|
||||
.preValidationInit(req as any)
|
||||
.catch(function (err: Error) {
|
||||
done();
|
||||
|
@ -73,19 +84,33 @@ describe("test totp register", function () {
|
|||
req.session.auth.first_factor = false;
|
||||
req.session.auth.email = undefined;
|
||||
|
||||
new RegistrationHandler(vars.logger, vars.userDataStore, vars.totpHandler)
|
||||
new RegistrationHandler(vars.logger, vars.userDataStore, vars.totpHandler,
|
||||
vars.config.totp)
|
||||
.preValidationInit(req as any)
|
||||
.catch(function (err: Error) {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("should succeed if first factor passed, userid and email are provided", function (done) {
|
||||
new RegistrationHandler(vars.logger, vars.userDataStore, vars.totpHandler)
|
||||
.preValidationInit(req as any)
|
||||
.then(function (identity: Identity) {
|
||||
done();
|
||||
it("should succeed if first factor passed, userid and email are provided",
|
||||
function () {
|
||||
return new RegistrationHandler(vars.logger, vars.userDataStore,
|
||||
vars.totpHandler, vars.config.totp)
|
||||
.preValidationInit(req as any);
|
||||
});
|
||||
});
|
||||
|
||||
describe("test totp registration post validation", function () {
|
||||
it("should generate a secret using userId as label and issuer defined in config", function () {
|
||||
vars.config.totp = {
|
||||
issuer: "issuer"
|
||||
};
|
||||
return new RegistrationHandler(vars.logger, vars.userDataStore,
|
||||
vars.totpHandler, vars.config.totp)
|
||||
.postValidationResponse(req as any, res as any)
|
||||
.then(function() {
|
||||
Assert(mocks.totpHandler.generateStub.calledWithExactly("user", "issuer"));
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
14
test/features/registration.feature
Normal file
14
test/features/registration.feature
Normal file
|
@ -0,0 +1,14 @@
|
|||
Feature: Register secret for second factor
|
||||
|
||||
Scenario: Register a TOTP secret with correct label and issuer
|
||||
Given I visit "https://auth.test.local:8080/"
|
||||
And I login with user "john" and password "password"
|
||||
When I register a TOTP secret called "Sec0"
|
||||
Then the otpauth url has label "john" and issuer "authelia.com"
|
||||
|
||||
@needs-totp_issuer-config
|
||||
Scenario: Register a TOTP secret with correct label and custom issuer
|
||||
Given I visit "https://auth.test.local:8080/"
|
||||
And I login with user "john" and password "password"
|
||||
When I register a TOTP secret called "Sec0"
|
||||
Then the otpauth url has label "john" and issuer "custom.com"
|
|
@ -43,6 +43,14 @@ Cucumber.defineSupportCode(function ({ After, Before }) {
|
|||
");
|
||||
}
|
||||
|
||||
function createCustomTotpIssuerConfiguration(): BluebirdPromise<void> {
|
||||
return exec("\
|
||||
cat config.template.yml > config.test.yml && \
|
||||
echo 'totp:' >> config.test.yml && \
|
||||
echo ' issuer: custom.com' >> config.test.yml \
|
||||
");
|
||||
}
|
||||
|
||||
function declareNeedsConfiguration(tag: string, cb: () => BluebirdPromise<void>) {
|
||||
Before({ tags: "@needs-" + tag + "-config", timeout: 20 * 1000 }, function () {
|
||||
return cb()
|
||||
|
@ -62,6 +70,7 @@ Cucumber.defineSupportCode(function ({ After, Before }) {
|
|||
declareNeedsConfiguration("regulation", createRegulationConfiguration);
|
||||
declareNeedsConfiguration("inactivity", createInactivityConfiguration);
|
||||
declareNeedsConfiguration("single_factor", createSingleFactorConfiguration);
|
||||
declareNeedsConfiguration("totp_issuer", createCustomTotpIssuerConfiguration);
|
||||
|
||||
function registerUser(context: any, username: string) {
|
||||
let secret: Speakeasy.Key;
|
||||
|
@ -72,7 +81,7 @@ Cucumber.defineSupportCode(function ({ After, Before }) {
|
|||
const userDataStore = new UserDataStore(collectionFactory);
|
||||
|
||||
const generator = new TotpHandler(Speakeasy);
|
||||
secret = generator.generate();
|
||||
secret = generator.generate("user", "authelia.com");
|
||||
return userDataStore.saveTOTPSecret(username, secret);
|
||||
})
|
||||
.then(function () {
|
||||
|
|
15
test/features/step_definitions/registration.ts
Normal file
15
test/features/step_definitions/registration.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
import Cucumber = require("cucumber");
|
||||
import seleniumWebdriver = require("selenium-webdriver");
|
||||
import Assert = require("assert");
|
||||
|
||||
Cucumber.defineSupportCode(function ({ Given, When, Then }) {
|
||||
When("the otpauth url has label {stringInDoubleQuotes} and issuer \
|
||||
{stringInDoubleQuotes}", function (label: string, issuer: string) {
|
||||
return this.driver.findElement(seleniumWebdriver.By.id("qrcode"))
|
||||
.getAttribute("title")
|
||||
.then(function (title: string) {
|
||||
const re = `^otpauth://totp/${label}\\?secret=[A-Z0-9]+&issuer=${issuer}$`;
|
||||
Assert(new RegExp(re).test(title));
|
||||
})
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue
Block a user