Check TOTP token with window of 1
A window of 1 means the token is checked against current time slot T as well as at time slot T-1 and T+1. A time slot is 30 seconds by default in Authelia.pull/142/head
parent
c02d9b4a6e
commit
3a88ca95b8
|
@ -71,7 +71,6 @@
|
|||
"@types/request-promise": "^4.1.38",
|
||||
"@types/selenium-webdriver": "^3.0.4",
|
||||
"@types/sinon": "^2.2.1",
|
||||
"@types/speakeasy": "^2.0.1",
|
||||
"@types/tmp": "0.0.33",
|
||||
"@types/winston": "^2.3.2",
|
||||
"@types/yamljs": "^0.2.30",
|
||||
|
|
|
@ -1,16 +1,24 @@
|
|||
|
||||
import * as speakeasy from "speakeasy";
|
||||
import { Speakeasy } from "../../types/Dependencies";
|
||||
import Speakeasy = require("speakeasy");
|
||||
import BluebirdPromise = require("bluebird");
|
||||
import { TOTPSecret } from "../../types/TOTPSecret";
|
||||
|
||||
interface GenerateSecretOptions {
|
||||
length?: number;
|
||||
symbols?: boolean;
|
||||
otpauth_url?: boolean;
|
||||
name?: string;
|
||||
issuer?: string;
|
||||
}
|
||||
|
||||
export class TOTPGenerator {
|
||||
private speakeasy: Speakeasy;
|
||||
private speakeasy: typeof Speakeasy;
|
||||
|
||||
constructor(speakeasy: Speakeasy) {
|
||||
constructor(speakeasy: typeof Speakeasy) {
|
||||
this.speakeasy = speakeasy;
|
||||
}
|
||||
|
||||
generate(options?: speakeasy.GenerateOptions): speakeasy.Key {
|
||||
generate(options?: GenerateSecretOptions): TOTPSecret {
|
||||
return this.speakeasy.generateSecret(options);
|
||||
}
|
||||
}
|
|
@ -1,23 +1,27 @@
|
|||
|
||||
import { Speakeasy } from "../../types/Dependencies";
|
||||
import Speakeasy = require("speakeasy");
|
||||
import BluebirdPromise = require("bluebird");
|
||||
|
||||
const TOTP_ENCODING = "base32";
|
||||
const WINDOW: number = 1;
|
||||
|
||||
export class TOTPValidator {
|
||||
private speakeasy: Speakeasy;
|
||||
private speakeasy: typeof Speakeasy;
|
||||
|
||||
constructor(speakeasy: Speakeasy) {
|
||||
constructor(speakeasy: typeof Speakeasy) {
|
||||
this.speakeasy = speakeasy;
|
||||
}
|
||||
|
||||
validate(token: string, secret: string): BluebirdPromise<void> {
|
||||
const real_token = this.speakeasy.totp({
|
||||
const isValid = this.speakeasy.totp.verify({
|
||||
secret: secret,
|
||||
encoding: TOTP_ENCODING
|
||||
});
|
||||
encoding: TOTP_ENCODING,
|
||||
token: token,
|
||||
window: WINDOW
|
||||
} as any);
|
||||
|
||||
if (token == real_token) return BluebirdPromise.resolve();
|
||||
return BluebirdPromise.reject(new Error("Wrong challenge"));
|
||||
if (isValid)
|
||||
return BluebirdPromise.resolve();
|
||||
else
|
||||
return BluebirdPromise.reject(new Error("Wrong TOTP token."));
|
||||
}
|
||||
}
|
|
@ -1,26 +1,37 @@
|
|||
|
||||
import { TOTPValidator } from "../src/lib/TOTPValidator";
|
||||
import sinon = require("sinon");
|
||||
import Promise = require("bluebird");
|
||||
import SpeakeasyMock = require("./mocks/speakeasy");
|
||||
import Sinon = require("sinon");
|
||||
import Speakeasy = require("speakeasy");
|
||||
|
||||
describe("test TOTP validation", function() {
|
||||
let totpValidator: TOTPValidator;
|
||||
let totpValidateStub: Sinon.SinonStub;
|
||||
|
||||
beforeEach(() => {
|
||||
SpeakeasyMock.totp.returns("token");
|
||||
totpValidator = new TOTPValidator(SpeakeasyMock as any);
|
||||
totpValidateStub = Sinon.stub(Speakeasy.totp, "verify");
|
||||
totpValidator = new TOTPValidator(Speakeasy);
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
totpValidateStub.restore();
|
||||
});
|
||||
|
||||
it("should validate the TOTP token", function() {
|
||||
const totp_secret = "NBD2ZV64R9UV1O7K";
|
||||
const token = "token";
|
||||
totpValidateStub.withArgs({
|
||||
secret: totp_secret,
|
||||
token: token,
|
||||
encoding: "base32",
|
||||
window: 1
|
||||
}).returns(true);
|
||||
return totpValidator.validate(token, totp_secret);
|
||||
});
|
||||
|
||||
it("should not validate a wrong TOTP token", function(done) {
|
||||
const totp_secret = "NBD2ZV64R9UV1O7K";
|
||||
const token = "wrong token";
|
||||
totpValidateStub.returns(false);
|
||||
totpValidator.validate(token, totp_secret)
|
||||
.catch(function() {
|
||||
done();
|
||||
|
|
|
@ -29,7 +29,12 @@ describe("test user data store", function () {
|
|||
totpSecret = {
|
||||
ascii: "abc",
|
||||
base32: "ABCDKZLEFZGREJK",
|
||||
otpauth_url: "totp://test"
|
||||
otpauth_url: "totp://test",
|
||||
google_auth_qr: "dummy",
|
||||
hex: "dummy",
|
||||
qr_code_ascii: "dummy",
|
||||
qr_code_base32: "dummy",
|
||||
qr_code_hex: "dummy"
|
||||
};
|
||||
|
||||
u2fRegistration = {
|
||||
|
|
|
@ -1,6 +1,11 @@
|
|||
|
||||
export interface TOTPSecret {
|
||||
base32: string;
|
||||
ascii: string;
|
||||
otpauth_url?: string;
|
||||
}
|
||||
hex: string;
|
||||
base32: string;
|
||||
qr_code_ascii: string;
|
||||
qr_code_hex: string;
|
||||
qr_code_base32: string;
|
||||
google_auth_qr: string;
|
||||
otpauth_url: string;
|
||||
}
|
|
@ -0,0 +1,96 @@
|
|||
declare module "speakeasy" {
|
||||
export = speakeasy
|
||||
|
||||
interface SharedOptions {
|
||||
encoding?: string
|
||||
algorithm?: string
|
||||
}
|
||||
|
||||
interface DigestOptions extends SharedOptions {
|
||||
secret: string
|
||||
counter: number
|
||||
}
|
||||
|
||||
interface HOTPOptions extends SharedOptions {
|
||||
secret: string
|
||||
counter: number
|
||||
digest?: Buffer
|
||||
digits?: number
|
||||
}
|
||||
|
||||
interface HOTPVerifyOptions extends SharedOptions {
|
||||
secret: string
|
||||
token: string
|
||||
counter: number
|
||||
digits?: number
|
||||
window?: number
|
||||
}
|
||||
|
||||
interface TOTPOptions extends SharedOptions {
|
||||
secret: string
|
||||
time?: number
|
||||
step?: number
|
||||
epoch?: number
|
||||
counter?: number
|
||||
digits?: number
|
||||
}
|
||||
|
||||
interface TOTPVerifyOptions extends SharedOptions {
|
||||
secret: string
|
||||
token: string
|
||||
time?: number
|
||||
step?: number
|
||||
epoch?: number
|
||||
counter?: number
|
||||
digits?: number
|
||||
window?: number
|
||||
}
|
||||
|
||||
interface GenerateSecretOptions {
|
||||
length?: number
|
||||
symbols?: boolean
|
||||
otpauth_url?: boolean
|
||||
name?: string
|
||||
issuer?: string
|
||||
}
|
||||
|
||||
interface GeneratedSecret {
|
||||
ascii: string
|
||||
hex: string
|
||||
base32: string
|
||||
qr_code_ascii: string
|
||||
qr_code_hex: string
|
||||
qr_code_base32: string
|
||||
google_auth_qr: string
|
||||
otpauth_url: string
|
||||
}
|
||||
|
||||
interface OTPAuthURLOptions extends SharedOptions {
|
||||
secret: string
|
||||
label: string
|
||||
type?: string
|
||||
counter?: number
|
||||
issuer?: string
|
||||
digits?: number
|
||||
period?: number
|
||||
}
|
||||
|
||||
interface Speakeasy {
|
||||
digest: (options: DigestOptions) => Buffer
|
||||
hotp: {
|
||||
(options: HOTPOptions): string,
|
||||
verifyDelta: (options: HOTPVerifyOptions) => boolean,
|
||||
verify: (options: HOTPVerifyOptions) => boolean,
|
||||
}
|
||||
totp: {
|
||||
(options: TOTPOptions): string
|
||||
verifyDelta: (options: TOTPVerifyOptions) => boolean,
|
||||
verify: (options: TOTPVerifyOptions) => boolean,
|
||||
}
|
||||
generateSecret: (options?: GenerateSecretOptions) => GeneratedSecret
|
||||
generateSecretASCII: (length?: number, symbols?: boolean) => string
|
||||
otpauthURL: (options: OTPAuthURLOptions) => string
|
||||
}
|
||||
|
||||
const speakeasy: Speakeasy
|
||||
}
|
|
@ -21,6 +21,7 @@ function CustomWorld() {
|
|||
};
|
||||
|
||||
this.setFieldTo = function (fieldName: string, content: string) {
|
||||
const that = this;
|
||||
return this.driver.findElement(seleniumWebdriver.By.id(fieldName))
|
||||
.sendKeys(content);
|
||||
};
|
||||
|
@ -49,22 +50,19 @@ function CustomWorld() {
|
|||
.findElement(seleniumWebdriver.By.tagName("button"))
|
||||
.findElement(seleniumWebdriver.By.xpath("//button[contains(.,'" + buttonText + "')]"))
|
||||
.click();
|
||||
})
|
||||
.then(function () {
|
||||
return that.driver.sleep(1000);
|
||||
});
|
||||
};
|
||||
|
||||
this.waitUntilUrlContains = function(url: string) {
|
||||
this.waitUntilUrlContains = function (url: string) {
|
||||
const that = this;
|
||||
return this.driver.wait(seleniumWebdriver.until.urlIs(url), 15000)
|
||||
.then(function() {}, function(err: Error) {
|
||||
that.driver.getCurrentUrl()
|
||||
.then(function(current: string) {
|
||||
console.error("====> Error due to: %s (current) != %s (expected)", current, url);
|
||||
.then(function () { }, function (err: Error) {
|
||||
that.driver.getCurrentUrl()
|
||||
.then(function (current: string) {
|
||||
console.error("====> Error due to: %s (current) != %s (expected)", current, url);
|
||||
});
|
||||
return BluebirdPromise.reject(err);
|
||||
});
|
||||
return BluebirdPromise.reject(err);
|
||||
});
|
||||
};
|
||||
|
||||
this.loginWithUserPassword = function (username: string, password: string) {
|
||||
|
|
Loading…
Reference in New Issue