Merge pull request #185 from clems4ever/totp-issuer-and-label
Use issuer and label when generating otpauthURL for TOTPpull/177/head
commit
c1afde83f6
|
@ -1,14 +1,6 @@
|
||||||
import { TOTPSecret } from "../../../../types/TOTPSecret";
|
import { TOTPSecret } from "../../../../types/TOTPSecret";
|
||||||
|
|
||||||
export interface GenerateSecretOptions {
|
|
||||||
length?: number;
|
|
||||||
symbols?: boolean;
|
|
||||||
otpauth_url?: boolean;
|
|
||||||
name?: string;
|
|
||||||
issuer?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ITotpHandler {
|
export interface ITotpHandler {
|
||||||
generate(options?: GenerateSecretOptions): TOTPSecret;
|
generate(label: string, issuer: string): TOTPSecret;
|
||||||
validate(token: string, secret: string): boolean;
|
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 { TOTPSecret } from "../../../../types/TOTPSecret";
|
||||||
import Speakeasy = require("speakeasy");
|
import Speakeasy = require("speakeasy");
|
||||||
|
|
||||||
|
@ -12,8 +12,17 @@ export class TotpHandler implements ITotpHandler {
|
||||||
this.speakeasy = speakeasy;
|
this.speakeasy = speakeasy;
|
||||||
}
|
}
|
||||||
|
|
||||||
generate(options?: GenerateSecretOptions): TOTPSecret {
|
generate(label: string, issuer: string): TOTPSecret {
|
||||||
return this.speakeasy.generateSecret(options);
|
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 {
|
validate(token: string, secret: string): boolean {
|
||||||
|
@ -22,6 +31,6 @@ export class TotpHandler implements ITotpHandler {
|
||||||
encoding: TOTP_ENCODING,
|
encoding: TOTP_ENCODING,
|
||||||
token: token,
|
token: token,
|
||||||
window: WINDOW
|
window: WINDOW
|
||||||
} as any);
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,46 +1,46 @@
|
||||||
export interface UserLdapConfiguration {
|
export interface UserLdapConfiguration {
|
||||||
url: string;
|
url: string;
|
||||||
base_dn: string;
|
base_dn: string;
|
||||||
|
|
||||||
additional_users_dn?: string;
|
additional_users_dn?: string;
|
||||||
users_filter?: string;
|
users_filter?: string;
|
||||||
|
|
||||||
additional_groups_dn?: string;
|
additional_groups_dn?: string;
|
||||||
groups_filter?: string;
|
groups_filter?: string;
|
||||||
|
|
||||||
group_name_attribute?: string;
|
group_name_attribute?: string;
|
||||||
mail_attribute?: string;
|
mail_attribute?: string;
|
||||||
|
|
||||||
user: string; // admin username
|
user: string; // admin username
|
||||||
password: string; // admin password
|
password: string; // admin password
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LdapConfiguration {
|
export interface LdapConfiguration {
|
||||||
url: string;
|
url: string;
|
||||||
|
|
||||||
users_dn: string;
|
users_dn: string;
|
||||||
users_filter: string;
|
users_filter: string;
|
||||||
|
|
||||||
groups_dn: string;
|
groups_dn: string;
|
||||||
groups_filter: string;
|
groups_filter: string;
|
||||||
|
|
||||||
group_name_attribute: string;
|
group_name_attribute: string;
|
||||||
mail_attribute: string;
|
mail_attribute: string;
|
||||||
|
|
||||||
user: string; // admin username
|
user: string; // admin username
|
||||||
password: string; // admin password
|
password: string; // admin password
|
||||||
}
|
}
|
||||||
|
|
||||||
type UserName = string;
|
type UserName = string;
|
||||||
type GroupName = string;
|
type GroupName = string;
|
||||||
type DomainPattern = string;
|
type DomainPattern = string;
|
||||||
|
|
||||||
export type ACLPolicy = 'deny' | 'allow';
|
export type ACLPolicy = 'deny' | 'allow';
|
||||||
|
|
||||||
export type ACLRule = {
|
export type ACLRule = {
|
||||||
domain: string;
|
domain: string;
|
||||||
policy: ACLPolicy;
|
policy: ACLPolicy;
|
||||||
resources?: string[];
|
resources?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ACLDefaultRules = ACLRule[];
|
export type ACLDefaultRules = ACLRule[];
|
||||||
|
@ -48,101 +48,107 @@ export type ACLGroupsRules = { [group: string]: ACLRule[]; };
|
||||||
export type ACLUsersRules = { [user: string]: ACLRule[]; };
|
export type ACLUsersRules = { [user: string]: ACLRule[]; };
|
||||||
|
|
||||||
export interface ACLConfiguration {
|
export interface ACLConfiguration {
|
||||||
default_policy?: ACLPolicy;
|
default_policy?: ACLPolicy;
|
||||||
any?: ACLDefaultRules;
|
any?: ACLDefaultRules;
|
||||||
groups?: ACLGroupsRules;
|
groups?: ACLGroupsRules;
|
||||||
users?: ACLUsersRules;
|
users?: ACLUsersRules;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SessionRedisOptions {
|
export interface SessionRedisOptions {
|
||||||
host: string;
|
host: string;
|
||||||
port: number;
|
port: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SessionCookieConfiguration {
|
interface SessionCookieConfiguration {
|
||||||
secret: string;
|
secret: string;
|
||||||
expiration?: number;
|
expiration?: number;
|
||||||
inactivity?: number;
|
inactivity?: number;
|
||||||
domain?: string;
|
domain?: string;
|
||||||
redis?: SessionRedisOptions;
|
redis?: SessionRedisOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EmailNotifierConfiguration {
|
export interface EmailNotifierConfiguration {
|
||||||
username: string;
|
username: string;
|
||||||
password: string;
|
password: string;
|
||||||
sender: string;
|
sender: string;
|
||||||
service: string;
|
service: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SmtpNotifierConfiguration {
|
export interface SmtpNotifierConfiguration {
|
||||||
username?: string;
|
username?: string;
|
||||||
password?: string;
|
password?: string;
|
||||||
host: string;
|
host: string;
|
||||||
port: number;
|
port: number;
|
||||||
secure: boolean;
|
secure: boolean;
|
||||||
sender: string;
|
sender: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FileSystemNotifierConfiguration {
|
export interface FileSystemNotifierConfiguration {
|
||||||
filename: string;
|
filename: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface NotifierConfiguration {
|
export interface NotifierConfiguration {
|
||||||
email?: EmailNotifierConfiguration;
|
email?: EmailNotifierConfiguration;
|
||||||
smtp?: SmtpNotifierConfiguration;
|
smtp?: SmtpNotifierConfiguration;
|
||||||
filesystem?: FileSystemNotifierConfiguration;
|
filesystem?: FileSystemNotifierConfiguration;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MongoStorageConfiguration {
|
export interface MongoStorageConfiguration {
|
||||||
url: string;
|
url: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LocalStorageConfiguration {
|
export interface LocalStorageConfiguration {
|
||||||
path?: string;
|
path?: string;
|
||||||
in_memory?: boolean;
|
in_memory?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface StorageConfiguration {
|
export interface StorageConfiguration {
|
||||||
local?: LocalStorageConfiguration;
|
local?: LocalStorageConfiguration;
|
||||||
mongo?: MongoStorageConfiguration;
|
mongo?: MongoStorageConfiguration;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RegulationConfiguration {
|
export interface RegulationConfiguration {
|
||||||
max_retries: number;
|
max_retries: number;
|
||||||
find_time: number;
|
find_time: number;
|
||||||
ban_time: number;
|
ban_time: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare type AuthenticationMethod = 'two_factor' | 'single_factor';
|
declare type AuthenticationMethod = 'two_factor' | 'single_factor';
|
||||||
declare type AuthenticationMethodPerSubdomain = { [subdomain: string]: AuthenticationMethod }
|
declare type AuthenticationMethodPerSubdomain = { [subdomain: string]: AuthenticationMethod }
|
||||||
|
|
||||||
export interface AuthenticationMethodsConfiguration {
|
export interface AuthenticationMethodsConfiguration {
|
||||||
default_method: AuthenticationMethod;
|
default_method: AuthenticationMethod;
|
||||||
per_subdomain_methods?: AuthenticationMethodPerSubdomain;
|
per_subdomain_methods?: AuthenticationMethodPerSubdomain;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TOTPConfiguration {
|
||||||
|
issuer: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UserConfiguration {
|
export interface UserConfiguration {
|
||||||
port?: number;
|
port?: number;
|
||||||
logs_level?: string;
|
logs_level?: string;
|
||||||
ldap: UserLdapConfiguration;
|
ldap: UserLdapConfiguration;
|
||||||
session: SessionCookieConfiguration;
|
session: SessionCookieConfiguration;
|
||||||
storage: StorageConfiguration;
|
storage: StorageConfiguration;
|
||||||
notifier: NotifierConfiguration;
|
notifier: NotifierConfiguration;
|
||||||
authentication_methods?: AuthenticationMethodsConfiguration;
|
authentication_methods?: AuthenticationMethodsConfiguration;
|
||||||
access_control?: ACLConfiguration;
|
access_control?: ACLConfiguration;
|
||||||
regulation: RegulationConfiguration;
|
regulation: RegulationConfiguration;
|
||||||
default_redirection_url?: string;
|
default_redirection_url?: string;
|
||||||
|
totp?: TOTPConfiguration;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AppConfiguration {
|
export interface AppConfiguration {
|
||||||
port: number;
|
port: number;
|
||||||
logs_level: string;
|
logs_level: string;
|
||||||
ldap: LdapConfiguration;
|
ldap: LdapConfiguration;
|
||||||
session: SessionCookieConfiguration;
|
session: SessionCookieConfiguration;
|
||||||
storage: StorageConfiguration;
|
storage: StorageConfiguration;
|
||||||
notifier: NotifierConfiguration;
|
notifier: NotifierConfiguration;
|
||||||
authentication_methods: AuthenticationMethodsConfiguration;
|
authentication_methods: AuthenticationMethodsConfiguration;
|
||||||
access_control?: ACLConfiguration;
|
access_control?: ACLConfiguration;
|
||||||
regulation: RegulationConfiguration;
|
regulation: RegulationConfiguration;
|
||||||
default_redirection_url?: string;
|
default_redirection_url?: string;
|
||||||
|
totp: TOTPConfiguration;
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@ import {
|
||||||
} from "./Configuration";
|
} from "./Configuration";
|
||||||
import Util = require("util");
|
import Util = require("util");
|
||||||
import { ACLAdapter } from "./adapters/ACLAdapter";
|
import { ACLAdapter } from "./adapters/ACLAdapter";
|
||||||
|
import { TOTPAdapter } from "./adapters/TOTPAdapter";
|
||||||
import { AuthenticationMethodsAdapter } from "./adapters/AuthenticationMethodsAdapter";
|
import { AuthenticationMethodsAdapter } from "./adapters/AuthenticationMethodsAdapter";
|
||||||
import { Validator } from "./Validator";
|
import { Validator } from "./Validator";
|
||||||
|
|
||||||
|
@ -63,6 +64,7 @@ function adaptFromUserConfiguration(userConfiguration: UserConfiguration)
|
||||||
const ldapConfiguration = adaptLdapConfiguration(userConfiguration.ldap);
|
const ldapConfiguration = adaptLdapConfiguration(userConfiguration.ldap);
|
||||||
const authenticationMethods = AuthenticationMethodsAdapter
|
const authenticationMethods = AuthenticationMethodsAdapter
|
||||||
.adapt(userConfiguration.authentication_methods);
|
.adapt(userConfiguration.authentication_methods);
|
||||||
|
const totpConfiguration = TOTPAdapter.adapt(userConfiguration.totp);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
port: port,
|
port: port,
|
||||||
|
@ -83,7 +85,8 @@ function adaptFromUserConfiguration(userConfiguration: UserConfiguration)
|
||||||
access_control: ACLAdapter.adapt(userConfiguration.access_control),
|
access_control: ACLAdapter.adapt(userConfiguration.access_control),
|
||||||
regulation: userConfiguration.regulation,
|
regulation: userConfiguration.regulation,
|
||||||
authentication_methods: authenticationMethods,
|
authentication_methods: authenticationMethods,
|
||||||
default_redirection_url: userConfiguration.default_redirection_url
|
default_redirection_url: userConfiguration.default_redirection_url,
|
||||||
|
totp: totpConfiguration
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 { IUserDataStore } from "../../../../storage/IUserDataStore";
|
||||||
import { ITotpHandler } from "../../../../authentication/totp/ITotpHandler";
|
import { ITotpHandler } from "../../../../authentication/totp/ITotpHandler";
|
||||||
import { TOTPSecret } from "../../../../../../types/TOTPSecret";
|
import { TOTPSecret } from "../../../../../../types/TOTPSecret";
|
||||||
|
import { TOTPConfiguration } from "../../../../configuration/Configuration";
|
||||||
|
|
||||||
|
|
||||||
export default class RegistrationHandler implements IdentityValidable {
|
export default class RegistrationHandler implements IdentityValidable {
|
||||||
private logger: IRequestLogger;
|
private logger: IRequestLogger;
|
||||||
private userDataStore: IUserDataStore;
|
private userDataStore: IUserDataStore;
|
||||||
private totp: ITotpHandler;
|
private totp: ITotpHandler;
|
||||||
|
private configuration: TOTPConfiguration;
|
||||||
|
|
||||||
constructor(logger: IRequestLogger,
|
constructor(logger: IRequestLogger,
|
||||||
userDataStore: IUserDataStore,
|
userDataStore: IUserDataStore,
|
||||||
totp: ITotpHandler) {
|
totp: ITotpHandler, configuration: TOTPConfiguration) {
|
||||||
this.logger = logger;
|
this.logger = logger;
|
||||||
this.userDataStore = userDataStore;
|
this.userDataStore = userDataStore;
|
||||||
this.totp = totp;
|
this.totp = totp;
|
||||||
|
this.configuration = configuration;
|
||||||
}
|
}
|
||||||
|
|
||||||
challenge(): string {
|
challenge(): string {
|
||||||
|
@ -70,22 +73,24 @@ export default class RegistrationHandler implements IdentityValidable {
|
||||||
return FirstFactorValidator.validate(req, this.logger);
|
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;
|
const that = this;
|
||||||
let secret: TOTPSecret;
|
let secret: TOTPSecret;
|
||||||
let userId: string;
|
let userId: string;
|
||||||
return new BluebirdPromise(function (resolve, reject) {
|
return new BluebirdPromise(function (resolve, reject) {
|
||||||
const authSession = AuthenticationSessionHandler.get(req, that.logger);
|
const authSession = AuthenticationSessionHandler.get(req, that.logger);
|
||||||
const challenge = authSession.identity_check.challenge;
|
userId = authSession.userid;
|
||||||
userId = authSession.identity_check.userid;
|
|
||||||
|
|
||||||
if (challenge != Constants.CHALLENGE || !userId) {
|
if (authSession.identity_check.challenge != Constants.CHALLENGE
|
||||||
|
|| !userId)
|
||||||
return reject(new Error("Bad challenge."));
|
return reject(new Error("Bad challenge."));
|
||||||
}
|
|
||||||
resolve();
|
resolve();
|
||||||
})
|
})
|
||||||
.then(function () {
|
.then(function () {
|
||||||
secret = that.totp.generate();
|
secret = that.totp.generate(userId,
|
||||||
|
that.configuration.issuer);
|
||||||
that.logger.debug(req, "Save the TOTP secret in DB");
|
that.logger.debug(req, "Save the TOTP secret in DB");
|
||||||
return that.userDataStore.saveTOTPSecret(userId, secret);
|
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_START_GET,
|
||||||
Endpoints.SECOND_FACTOR_TOTP_IDENTITY_FINISH_GET,
|
Endpoints.SECOND_FACTOR_TOTP_IDENTITY_FINISH_GET,
|
||||||
new TOTPRegistrationIdentityHandler(vars.logger,
|
new TOTPRegistrationIdentityHandler(vars.logger,
|
||||||
vars.userDataStore, vars.totpHandler),
|
vars.userDataStore, vars.totpHandler, vars.config.totp),
|
||||||
vars);
|
vars);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,6 +16,9 @@ describe("test session configuration builder", function () {
|
||||||
users: {},
|
users: {},
|
||||||
groups: {}
|
groups: {}
|
||||||
},
|
},
|
||||||
|
totp: {
|
||||||
|
issuer: "authelia.com"
|
||||||
|
},
|
||||||
ldap: {
|
ldap: {
|
||||||
url: "ldap://ldap",
|
url: "ldap://ldap",
|
||||||
user: "user",
|
user: "user",
|
||||||
|
@ -90,6 +93,9 @@ describe("test session configuration builder", function () {
|
||||||
users: {},
|
users: {},
|
||||||
groups: {}
|
groups: {}
|
||||||
},
|
},
|
||||||
|
totp: {
|
||||||
|
issuer: "authelia.com"
|
||||||
|
},
|
||||||
ldap: {
|
ldap: {
|
||||||
url: "ldap://ldap",
|
url: "ldap://ldap",
|
||||||
user: "user",
|
user: "user",
|
||||||
|
|
|
@ -35,6 +35,9 @@ export class ServerVariablesMockBuilder {
|
||||||
authentication_methods: {
|
authentication_methods: {
|
||||||
default_method: "two_factor"
|
default_method: "two_factor"
|
||||||
},
|
},
|
||||||
|
totp: {
|
||||||
|
issuer: "authelia.com"
|
||||||
|
},
|
||||||
ldap: {
|
ldap: {
|
||||||
url: "ldap://ldap",
|
url: "ldap://ldap",
|
||||||
user: "user",
|
user: "user",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import Sinon = require("sinon");
|
import Sinon = require("sinon");
|
||||||
import BluebirdPromise = require("bluebird");
|
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";
|
import { TOTPSecret } from "../../types/TOTPSecret";
|
||||||
|
|
||||||
export class TotpHandlerStub implements ITotpHandler {
|
export class TotpHandlerStub implements ITotpHandler {
|
||||||
|
@ -12,8 +12,8 @@ export class TotpHandlerStub implements ITotpHandler {
|
||||||
this.validateStub = Sinon.stub();
|
this.validateStub = Sinon.stub();
|
||||||
}
|
}
|
||||||
|
|
||||||
generate(options?: GenerateSecretOptions): TOTPSecret {
|
generate(label: string, issuer: string): TOTPSecret {
|
||||||
return this.generateStub(options);
|
return this.generateStub(label, issuer);
|
||||||
}
|
}
|
||||||
|
|
||||||
validate(token: string, secret: string): boolean {
|
validate(token: string, secret: string): boolean {
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
import Sinon = require("sinon");
|
import Sinon = require("sinon");
|
||||||
import winston = require("winston");
|
|
||||||
import RegistrationHandler from "../../../../../src/lib/routes/secondfactor/totp/identity/RegistrationHandler";
|
import RegistrationHandler from "../../../../../src/lib/routes/secondfactor/totp/identity/RegistrationHandler";
|
||||||
import { Identity } from "../../../../../types/Identity";
|
import { Identity } from "../../../../../types/Identity";
|
||||||
import { UserDataStore } from "../../../../../src/lib/storage/UserDataStore";
|
import { UserDataStore } from "../../../../../src/lib/storage/UserDataStore";
|
||||||
import assert = require("assert");
|
|
||||||
import BluebirdPromise = require("bluebird");
|
import BluebirdPromise = require("bluebird");
|
||||||
import ExpressMock = require("../../../../mocks/express");
|
import ExpressMock = require("../../../../mocks/express");
|
||||||
import { ServerVariablesMock, ServerVariablesMockBuilder } from "../../../../mocks/ServerVariablesMockBuilder";
|
import { ServerVariablesMock, ServerVariablesMockBuilder }
|
||||||
|
from "../../../../mocks/ServerVariablesMockBuilder";
|
||||||
import { ServerVariables } from "../../../../../src/lib/ServerVariables";
|
import { ServerVariables } from "../../../../../src/lib/ServerVariables";
|
||||||
|
import Assert = require("assert");
|
||||||
|
|
||||||
describe("test totp register", function () {
|
describe("test totp register", function () {
|
||||||
let req: ExpressMock.RequestMock;
|
let req: ExpressMock.RequestMock;
|
||||||
|
@ -26,7 +26,11 @@ describe("test totp register", function () {
|
||||||
userid: "user",
|
userid: "user",
|
||||||
email: "user@example.com",
|
email: "user@example.com",
|
||||||
first_factor: true,
|
first_factor: true,
|
||||||
second_factor: false
|
second_factor: false,
|
||||||
|
identity_check: {
|
||||||
|
userid: "user",
|
||||||
|
challenge: "totp-register"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
req.headers = {};
|
req.headers = {};
|
||||||
|
@ -36,23 +40,29 @@ describe("test totp register", function () {
|
||||||
inMemoryOnly: true
|
inMemoryOnly: true
|
||||||
};
|
};
|
||||||
|
|
||||||
mocks.userDataStore.saveU2FRegistrationStub.returns(BluebirdPromise.resolve({}));
|
mocks.userDataStore.saveU2FRegistrationStub
|
||||||
mocks.userDataStore.retrieveU2FRegistrationStub.returns(BluebirdPromise.resolve({}));
|
.returns(BluebirdPromise.resolve({}));
|
||||||
mocks.userDataStore.produceIdentityValidationTokenStub.returns(BluebirdPromise.resolve({}));
|
mocks.userDataStore.retrieveU2FRegistrationStub
|
||||||
mocks.userDataStore.consumeIdentityValidationTokenStub.returns(BluebirdPromise.resolve({}));
|
.returns(BluebirdPromise.resolve({}));
|
||||||
mocks.userDataStore.saveTOTPSecretStub.returns(BluebirdPromise.resolve({}));
|
mocks.userDataStore.produceIdentityValidationTokenStub
|
||||||
|
.returns(BluebirdPromise.resolve({}));
|
||||||
|
mocks.userDataStore.consumeIdentityValidationTokenStub
|
||||||
|
.returns(BluebirdPromise.resolve({}));
|
||||||
|
mocks.userDataStore.saveTOTPSecretStub
|
||||||
|
.returns(BluebirdPromise.resolve({}));
|
||||||
|
|
||||||
res = ExpressMock.ResponseMock();
|
res = ExpressMock.ResponseMock();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("test totp registration check", test_registration_check);
|
describe("test totp registration pre validation", function () {
|
||||||
|
|
||||||
function test_registration_check() {
|
|
||||||
it("should fail if first_factor has not been passed", function () {
|
it("should fail if first_factor has not been passed", function () {
|
||||||
req.session.auth.first_factor = false;
|
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)
|
.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) {
|
.catch(function (err: Error) {
|
||||||
return BluebirdPromise.resolve();
|
return BluebirdPromise.resolve();
|
||||||
});
|
});
|
||||||
|
@ -62,7 +72,8 @@ describe("test totp register", function () {
|
||||||
req.session.auth.first_factor = false;
|
req.session.auth.first_factor = false;
|
||||||
req.session.auth.userid = undefined;
|
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)
|
.preValidationInit(req as any)
|
||||||
.catch(function (err: Error) {
|
.catch(function (err: Error) {
|
||||||
done();
|
done();
|
||||||
|
@ -73,19 +84,33 @@ describe("test totp register", function () {
|
||||||
req.session.auth.first_factor = false;
|
req.session.auth.first_factor = false;
|
||||||
req.session.auth.email = undefined;
|
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)
|
.preValidationInit(req as any)
|
||||||
.catch(function (err: Error) {
|
.catch(function (err: Error) {
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should succeed if first factor passed, userid and email are provided", function (done) {
|
it("should succeed if first factor passed, userid and email are provided",
|
||||||
new RegistrationHandler(vars.logger, vars.userDataStore, vars.totpHandler)
|
function () {
|
||||||
.preValidationInit(req as any)
|
return new RegistrationHandler(vars.logger, vars.userDataStore,
|
||||||
.then(function (identity: Identity) {
|
vars.totpHandler, vars.config.totp)
|
||||||
done();
|
.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"));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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>) {
|
function declareNeedsConfiguration(tag: string, cb: () => BluebirdPromise<void>) {
|
||||||
Before({ tags: "@needs-" + tag + "-config", timeout: 20 * 1000 }, function () {
|
Before({ tags: "@needs-" + tag + "-config", timeout: 20 * 1000 }, function () {
|
||||||
return cb()
|
return cb()
|
||||||
|
@ -62,6 +70,7 @@ Cucumber.defineSupportCode(function ({ After, Before }) {
|
||||||
declareNeedsConfiguration("regulation", createRegulationConfiguration);
|
declareNeedsConfiguration("regulation", createRegulationConfiguration);
|
||||||
declareNeedsConfiguration("inactivity", createInactivityConfiguration);
|
declareNeedsConfiguration("inactivity", createInactivityConfiguration);
|
||||||
declareNeedsConfiguration("single_factor", createSingleFactorConfiguration);
|
declareNeedsConfiguration("single_factor", createSingleFactorConfiguration);
|
||||||
|
declareNeedsConfiguration("totp_issuer", createCustomTotpIssuerConfiguration);
|
||||||
|
|
||||||
function registerUser(context: any, username: string) {
|
function registerUser(context: any, username: string) {
|
||||||
let secret: Speakeasy.Key;
|
let secret: Speakeasy.Key;
|
||||||
|
@ -72,7 +81,7 @@ Cucumber.defineSupportCode(function ({ After, Before }) {
|
||||||
const userDataStore = new UserDataStore(collectionFactory);
|
const userDataStore = new UserDataStore(collectionFactory);
|
||||||
|
|
||||||
const generator = new TotpHandler(Speakeasy);
|
const generator = new TotpHandler(Speakeasy);
|
||||||
secret = generator.generate();
|
secret = generator.generate("user", "authelia.com");
|
||||||
return userDataStore.saveTOTPSecret(username, secret);
|
return userDataStore.saveTOTPSecret(username, secret);
|
||||||
})
|
})
|
||||||
.then(function () {
|
.then(function () {
|
||||||
|
|
|
@ -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