diff --git a/.travis.yml b/.travis.yml index 0b8fecdb1..27105ca44 100644 --- a/.travis.yml +++ b/.travis.yml @@ -23,6 +23,7 @@ addons: - mx1.mail.example.com - mx2.mail.example.com - public.example.com + - authelia.example.com before_install: - npm install -g npm@'>=2.13.5' diff --git a/README.md b/README.md index 682b5ce3b..b70d69a71 100644 --- a/README.md +++ b/README.md @@ -4,9 +4,9 @@ [![Build](https://travis-ci.org/clems4ever/authelia.svg?branch=master)](https://travis-ci.org/clems4ever/authelia) [![Gitter](https://img.shields.io/gitter/room/badges/shields.svg)](https://gitter.im/authelia/general?utm_source=share-link&utm_medium=link&utm_campaign=share-link) -**Authelia** is a complete HTTP 2-factor authentication server for proxies like -nginx. It has been made to work with nginx [auth_request] module and is currently -used in production to secure internal services in a small docker swarm cluster. +**Authelia** is a complete HTTP 2-factor authentication server for proxies like +Nginx or Traefik. It has been designed to be proxy agnostic so that you can +use whichever proxy supporting authentication forwarding. # Table of Contents 1. [Features summary](#features-summary) diff --git a/doc/api_data.js b/doc/api_data.js index 2eacc2aaf..7aba8ada3 100644 --- a/doc/api_data.js +++ b/doc/api_data.js @@ -20,7 +20,7 @@ define({ "api": [ } }, "description": "

Serves the login page and create a create a cookie for the client.

", - "filename": "src/server/endpoints.ts", + "filename": "shared/api.ts", "groupTitle": "Authentication" }, { @@ -56,7 +56,7 @@ define({ "api": [ } }, "description": "

Log out the user and redirect to the URL.

", - "filename": "src/server/endpoints.ts", + "filename": "shared/api.ts", "groupTitle": "Authentication" }, { @@ -80,7 +80,7 @@ define({ "api": [ } }, "description": "

Serves the second factor page

", - "filename": "src/server/endpoints.ts", + "filename": "shared/api.ts", "groupTitle": "Authentication" }, { @@ -145,7 +145,7 @@ define({ "api": [ } }, "description": "

Verify credentials against the LDAP.

", - "filename": "src/server/endpoints.ts", + "filename": "shared/api.ts", "groupTitle": "Authentication", "header": { "fields": { @@ -169,7 +169,7 @@ define({ "api": [ "group": "PasswordReset", "version": "1.0.0", "description": "

Start password reset request.

", - "filename": "src/server/endpoints.ts", + "filename": "shared/api.ts", "groupTitle": "PasswordReset", "header": { "fields": { @@ -240,7 +240,7 @@ define({ "api": [ "group": "PasswordReset", "version": "1.0.0", "description": "

Serve a page that requires the username.

", - "filename": "src/server/endpoints.ts", + "filename": "shared/api.ts", "groupTitle": "PasswordReset", "header": { "fields": { @@ -277,7 +277,7 @@ define({ "api": [ } }, "description": "

Set a new password for the user.

", - "filename": "src/server/endpoints.ts", + "filename": "shared/api.ts", "groupTitle": "PasswordReset", "header": { "fields": { @@ -301,7 +301,7 @@ define({ "api": [ "group": "PasswordReset", "version": "1.0.0", "description": "

Start password reset request.

", - "filename": "src/server/endpoints.ts", + "filename": "shared/api.ts", "groupTitle": "PasswordReset", "header": { "fields": { @@ -366,7 +366,7 @@ define({ "api": [ "group": "TOTP", "version": "1.0.0", "description": "

Serves the TOTP registration page that displays the secret. The secret is a QRCode and a base32 secret.

", - "filename": "src/server/endpoints.ts", + "filename": "shared/api.ts", "groupTitle": "TOTP", "header": { "fields": { @@ -437,7 +437,7 @@ define({ "api": [ "group": "TOTP", "version": "1.0.0", "description": "

Initiates the identity validation

", - "filename": "src/server/endpoints.ts", + "filename": "shared/api.ts", "groupTitle": "TOTP", "header": { "fields": { @@ -521,7 +521,7 @@ define({ "api": [ "group": "Success 302", "optional": false, "field": "Redirect", - "description": "

to the URL that has been stored during last call to /verify.

" + "description": "

to the URL that has been stored during last call to /api/verify.

" } ] } @@ -549,7 +549,7 @@ define({ "api": [ } }, "description": "

Verify TOTP token. The user is authenticated upon success.

", - "filename": "src/server/endpoints.ts", + "filename": "shared/api.ts", "groupTitle": "TOTP", "header": { "fields": { @@ -579,7 +579,7 @@ define({ "api": [ "group": "Success 302", "optional": false, "field": "Redirect", - "description": "

to the URL that has been stored during last call to /verify.

" + "description": "

to the URL that has been stored during last call to /api/verify.

" } ] } @@ -607,7 +607,7 @@ define({ "api": [ } }, "description": "

Complete authentication request of the U2F device.

", - "filename": "src/server/endpoints.ts", + "filename": "shared/api.ts", "groupTitle": "U2F", "header": { "fields": { @@ -637,13 +637,13 @@ define({ "api": [ "group": "Success 302", "optional": false, "field": "Redirect", - "description": "

to the URL that has been stored during last call to /verify.

" + "description": "

to the URL that has been stored during last call to /api/verify.

" } ] } }, "description": "

Complete U2F registration request.

", - "filename": "src/server/endpoints.ts", + "filename": "shared/api.ts", "groupTitle": "U2F", "header": { "fields": { @@ -679,7 +679,7 @@ define({ "api": [ "name": "RequestU2FRegistration", "group": "U2F", "version": "1.0.0", - "filename": "src/server/endpoints.ts", + "filename": "shared/api.ts", "groupTitle": "U2F", "header": { "fields": { @@ -745,7 +745,7 @@ define({ "api": [ "group": "U2F", "version": "1.0.0", "description": "

Serves the U2F registration page that asks the user to touch the token of the U2F device.

", - "filename": "src/server/endpoints.ts", + "filename": "shared/api.ts", "groupTitle": "U2F", "header": { "fields": { @@ -850,7 +850,7 @@ define({ "api": [ } }, "description": "

Initiate an authentication request using a U2F device.

", - "filename": "src/server/endpoints.ts", + "filename": "shared/api.ts", "groupTitle": "U2F", "header": { "fields": { @@ -908,7 +908,7 @@ define({ "api": [ } }, "description": "

Initiate a U2F device registration request.

", - "filename": "src/server/endpoints.ts", + "filename": "shared/api.ts", "groupTitle": "U2F", "header": { "fields": { @@ -926,11 +926,24 @@ define({ "api": [ }, { "type": "get", - "url": "/verify", + "url": "/api/verify", "title": "Verify user authentication", "name": "VerifyAuthentication", "group": "Verification", "version": "1.0.0", + "parameter": { + "fields": { + "Parameter": [ + { + "group": "Parameter", + "type": "String", + "optional": false, + "field": "redirect", + "description": "

Optional parameter set to the url where the user is redirected if access is refused. It is mainly used by Traefik that does not control the redirection itself.

" + } + ] + } + }, "success": { "fields": { "Success 204": [ @@ -945,18 +958,26 @@ define({ "api": [ }, "error": { "fields": { + "Error 302": [ + { + "group": "Error 302", + "optional": false, + "field": "redirect", + "description": "

The user is redirected if redirect parameter is provided.

" + } + ], "Error 401": [ { "group": "Error 401", "optional": false, "field": "status", - "description": "

The user is not authenticated.

" + "description": "

The user get an error if access failed

" } ] } }, - "description": "

Verify that the user is authenticated, i.e., the two factors have been validated

", - "filename": "src/server/endpoints.ts", + "description": "

Verify that the user is authenticated, i.e., the two factors have been validated. If the user is authenticated the response headers Remote-User and Remote-Groups are set. Remote-User contains the user id of the currently logged in user and Remote-Groups a comma separated list of assigned groups.

", + "filename": "shared/api.ts", "groupTitle": "Verification", "header": { "fields": { diff --git a/doc/api_data.json b/doc/api_data.json index 527b67508..c6935ffe7 100644 --- a/doc/api_data.json +++ b/doc/api_data.json @@ -20,7 +20,7 @@ } }, "description": "

Serves the login page and create a create a cookie for the client.

", - "filename": "src/server/endpoints.ts", + "filename": "shared/api.ts", "groupTitle": "Authentication" }, { @@ -56,7 +56,7 @@ } }, "description": "

Log out the user and redirect to the URL.

", - "filename": "src/server/endpoints.ts", + "filename": "shared/api.ts", "groupTitle": "Authentication" }, { @@ -80,7 +80,7 @@ } }, "description": "

Serves the second factor page

", - "filename": "src/server/endpoints.ts", + "filename": "shared/api.ts", "groupTitle": "Authentication" }, { @@ -145,7 +145,7 @@ } }, "description": "

Verify credentials against the LDAP.

", - "filename": "src/server/endpoints.ts", + "filename": "shared/api.ts", "groupTitle": "Authentication", "header": { "fields": { @@ -169,7 +169,7 @@ "group": "PasswordReset", "version": "1.0.0", "description": "

Start password reset request.

", - "filename": "src/server/endpoints.ts", + "filename": "shared/api.ts", "groupTitle": "PasswordReset", "header": { "fields": { @@ -240,7 +240,7 @@ "group": "PasswordReset", "version": "1.0.0", "description": "

Serve a page that requires the username.

", - "filename": "src/server/endpoints.ts", + "filename": "shared/api.ts", "groupTitle": "PasswordReset", "header": { "fields": { @@ -277,7 +277,7 @@ } }, "description": "

Set a new password for the user.

", - "filename": "src/server/endpoints.ts", + "filename": "shared/api.ts", "groupTitle": "PasswordReset", "header": { "fields": { @@ -301,7 +301,7 @@ "group": "PasswordReset", "version": "1.0.0", "description": "

Start password reset request.

", - "filename": "src/server/endpoints.ts", + "filename": "shared/api.ts", "groupTitle": "PasswordReset", "header": { "fields": { @@ -366,7 +366,7 @@ "group": "TOTP", "version": "1.0.0", "description": "

Serves the TOTP registration page that displays the secret. The secret is a QRCode and a base32 secret.

", - "filename": "src/server/endpoints.ts", + "filename": "shared/api.ts", "groupTitle": "TOTP", "header": { "fields": { @@ -437,7 +437,7 @@ "group": "TOTP", "version": "1.0.0", "description": "

Initiates the identity validation

", - "filename": "src/server/endpoints.ts", + "filename": "shared/api.ts", "groupTitle": "TOTP", "header": { "fields": { @@ -521,7 +521,7 @@ "group": "Success 302", "optional": false, "field": "Redirect", - "description": "

to the URL that has been stored during last call to /verify.

" + "description": "

to the URL that has been stored during last call to /api/verify.

" } ] } @@ -549,7 +549,7 @@ } }, "description": "

Verify TOTP token. The user is authenticated upon success.

", - "filename": "src/server/endpoints.ts", + "filename": "shared/api.ts", "groupTitle": "TOTP", "header": { "fields": { @@ -579,7 +579,7 @@ "group": "Success 302", "optional": false, "field": "Redirect", - "description": "

to the URL that has been stored during last call to /verify.

" + "description": "

to the URL that has been stored during last call to /api/verify.

" } ] } @@ -607,7 +607,7 @@ } }, "description": "

Complete authentication request of the U2F device.

", - "filename": "src/server/endpoints.ts", + "filename": "shared/api.ts", "groupTitle": "U2F", "header": { "fields": { @@ -637,13 +637,13 @@ "group": "Success 302", "optional": false, "field": "Redirect", - "description": "

to the URL that has been stored during last call to /verify.

" + "description": "

to the URL that has been stored during last call to /api/verify.

" } ] } }, "description": "

Complete U2F registration request.

", - "filename": "src/server/endpoints.ts", + "filename": "shared/api.ts", "groupTitle": "U2F", "header": { "fields": { @@ -679,7 +679,7 @@ "name": "RequestU2FRegistration", "group": "U2F", "version": "1.0.0", - "filename": "src/server/endpoints.ts", + "filename": "shared/api.ts", "groupTitle": "U2F", "header": { "fields": { @@ -745,7 +745,7 @@ "group": "U2F", "version": "1.0.0", "description": "

Serves the U2F registration page that asks the user to touch the token of the U2F device.

", - "filename": "src/server/endpoints.ts", + "filename": "shared/api.ts", "groupTitle": "U2F", "header": { "fields": { @@ -850,7 +850,7 @@ } }, "description": "

Initiate an authentication request using a U2F device.

", - "filename": "src/server/endpoints.ts", + "filename": "shared/api.ts", "groupTitle": "U2F", "header": { "fields": { @@ -908,7 +908,7 @@ } }, "description": "

Initiate a U2F device registration request.

", - "filename": "src/server/endpoints.ts", + "filename": "shared/api.ts", "groupTitle": "U2F", "header": { "fields": { @@ -926,11 +926,24 @@ }, { "type": "get", - "url": "/verify", + "url": "/api/verify", "title": "Verify user authentication", "name": "VerifyAuthentication", "group": "Verification", "version": "1.0.0", + "parameter": { + "fields": { + "Parameter": [ + { + "group": "Parameter", + "type": "String", + "optional": false, + "field": "redirect", + "description": "

Optional parameter set to the url where the user is redirected if access is refused. It is mainly used by Traefik that does not control the redirection itself.

" + } + ] + } + }, "success": { "fields": { "Success 204": [ @@ -945,18 +958,26 @@ }, "error": { "fields": { + "Error 302": [ + { + "group": "Error 302", + "optional": false, + "field": "redirect", + "description": "

The user is redirected if redirect parameter is provided.

" + } + ], "Error 401": [ { "group": "Error 401", "optional": false, "field": "status", - "description": "

The user is not authenticated.

" + "description": "

The user get an error if access failed

" } ] } }, - "description": "

Verify that the user is authenticated, i.e., the two factors have been validated

", - "filename": "src/server/endpoints.ts", + "description": "

Verify that the user is authenticated, i.e., the two factors have been validated. If the user is authenticated the response headers Remote-User and Remote-Groups are set. Remote-User contains the user id of the currently logged in user and Remote-Groups a comma separated list of assigned groups.

", + "filename": "shared/api.ts", "groupTitle": "Verification", "header": { "fields": { diff --git a/doc/api_project.js b/doc/api_project.js index 9b4ecf09d..727258a09 100644 --- a/doc/api_project.js +++ b/doc/api_project.js @@ -1,14 +1,14 @@ define({ "title": "Authelia API documentation", "name": "authelia", - "version": "2.1.3", + "version": "3.7.0", "description": "2FA Single Sign-On server for nginx using LDAP, TOTP and U2F", "sampleUrl": false, "defaultVersion": "0.0.0", "apidoc": "0.3.0", "generator": { "name": "apidoc", - "time": "2017-06-11T20:41:36.025Z", + "time": "2017-12-04T21:38:44.927Z", "url": "http://apidocjs.com", "version": "0.17.6" } diff --git a/doc/api_project.json b/doc/api_project.json index b27e7e63e..a3017b24f 100644 --- a/doc/api_project.json +++ b/doc/api_project.json @@ -1,14 +1,14 @@ { "title": "Authelia API documentation", "name": "authelia", - "version": "2.1.3", + "version": "3.7.0", "description": "2FA Single Sign-On server for nginx using LDAP, TOTP and U2F", "sampleUrl": false, "defaultVersion": "0.0.0", "apidoc": "0.3.0", "generator": { "name": "apidoc", - "time": "2017-06-11T20:41:36.025Z", + "time": "2017-12-04T21:38:44.927Z", "url": "http://apidocjs.com", "version": "0.17.6" } diff --git a/example/nginx/portal/nginx.conf b/example/nginx/portal/nginx.conf index c7d4016b0..1ce5fcfc9 100644 --- a/example/nginx/portal/nginx.conf +++ b/example/nginx/portal/nginx.conf @@ -322,5 +322,25 @@ http { proxy_pass $upstream_headers; } } + + server { + listen 443 ssl; + server_name authelia.example.com; + + resolver 127.0.0.11 ipv6=off; + set $upstream_endpoint http://authelia; + + ssl on; + ssl_certificate /etc/ssl/server.crt; + ssl_certificate_key /etc/ssl/server.key; + + add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; + add_header X-Frame-Options "SAMEORIGIN"; + + location / { + proxy_set_header Host $http_host; + proxy_pass $upstream_endpoint; + } + } } diff --git a/server/src/lib/ErrorReplies.ts b/server/src/lib/ErrorReplies.ts index dc74abbe0..f1c5f4fd1 100644 --- a/server/src/lib/ErrorReplies.ts +++ b/server/src/lib/ErrorReplies.ts @@ -19,6 +19,15 @@ function replyWithError(req: express.Request, res: express.Response, }; } +export function redirectTo(redirectUrl: string, req: express.Request, + res: express.Response, logger: IRequestLogger) { + return function(err: Error) { + logger.error(req, "Error: %s", err.message); + logger.debug(req, "Redirecting to %s", redirectUrl); + res.redirect(redirectUrl); + }; +} + export function replyWithError400(req: express.Request, res: express.Response, logger: IRequestLogger) { return replyWithError(req, res, 400, logger); diff --git a/server/src/lib/routes/verify/get.ts b/server/src/lib/routes/verify/get.ts index 54f0c2ec5..79fda877e 100644 --- a/server/src/lib/routes/verify/get.ts +++ b/server/src/lib/routes/verify/get.ts @@ -5,6 +5,7 @@ import ErrorReplies = require("../../ErrorReplies"); import { ServerVariables } from "../../ServerVariables"; import GetWithSessionCookieMethod from "./get_session_cookie"; import GetWithBasicAuthMethod from "./get_basic_auth"; +import Constants = require("../../../../../shared/constants"); import { AuthenticationSessionHandler } from "../../AuthenticationSessionHandler"; @@ -50,6 +51,12 @@ function replyWith200(res: Express.Response) { }; } +function getRedirectParam(req: Express.Request) { + return req.query[Constants.REDIRECT_QUERY_PARAM] != "undefined" + ? req.query[Constants.REDIRECT_QUERY_PARAM] + : undefined; +} + export default function (vars: ServerVariables) { return function (req: Express.Request, res: Express.Response) : BluebirdPromise { @@ -66,7 +73,15 @@ export default function (vars: ServerVariables) { .catch(Exceptions.DomainAccessDenied, ErrorReplies .replyWithError403(req, res, vars.logger)) // The user is not yet authenticated -> 401 - .catch(ErrorReplies.replyWithError401(req, res, vars.logger)); + .catch(function (err) { + const redirectUrl = getRedirectParam(req); + if (redirectUrl) { + ErrorReplies.redirectTo(redirectUrl, req, res, vars.logger)(err); + } + else { + ErrorReplies.replyWithError401(req, res, vars.logger)(err); + } + }); }; } diff --git a/server/test/routes/verify/get.test.ts b/server/test/routes/verify/get.test.ts index 099e763b6..d46a0af6a 100644 --- a/server/test/routes/verify/get.test.ts +++ b/server/test/routes/verify/get.test.ts @@ -24,11 +24,11 @@ describe("test /api/verify endpoint", function () { res = ExpressMock.ResponseMock(); req.originalUrl = "/api/xxxx"; req.query = { - redirect: "http://redirect.url" + redirect: "undefined" }; AuthenticationSessionHandler.reset(req as any); req.headers.host = "secret.example.com"; - const s = ServerVariablesMockBuilder.build(true); + const s = ServerVariablesMockBuilder.build(false); mocks = s.mocks; vars = s.variables; authSession = AuthenticationSessionHandler.get(req as any, vars.logger); @@ -147,9 +147,6 @@ describe("test /api/verify endpoint", function () { describe("given user tries to access a single factor endpoint", function () { beforeEach(function () { - req.query = { - redirect: "http://redirect.url" - }; req.headers["host"] = "redirect.url"; mocks.config.authentication_methods.per_subdomain_methods = { "redirect.url": "single_factor" @@ -220,6 +217,36 @@ describe("test /api/verify endpoint", function () { }); }); + describe("response type 401 | 302", function() { + it("should return error code 401", function() { + mocks.accessController.isAccessAllowedMock.returns(true); + mocks.config.authentication_methods.default_method = "single_factor"; + mocks.ldapAuthenticator.authenticateStub.rejects(new Error( + "Invalid credentials")); + req.headers["proxy-authorization"] = "Basic am9objpwYXNzd29yZA=="; + + return VerifyGet.default(vars)(req as express.Request, res as any) + .then(function () { + Assert(res.status.calledWithExactly(401)); + }); + }); + + it("should redirect to provided redirection url", function() { + const REDIRECT_URL = "http://redirection_url.com"; + mocks.accessController.isAccessAllowedMock.returns(true); + mocks.config.authentication_methods.default_method = "single_factor"; + mocks.ldapAuthenticator.authenticateStub.rejects(new Error( + "Invalid credentials")); + req.headers["proxy-authorization"] = "Basic am9objpwYXNzd29yZA=="; + req.query["redirect"] = REDIRECT_URL; + + return VerifyGet.default(vars)(req as express.Request, res as any) + .then(function () { + Assert(res.redirect.calledWithExactly(REDIRECT_URL)); + }); + }); + }); + describe("with basic auth", function () { it("should authenticate correctly", function () { mocks.accessController.isAccessAllowedMock.returns(true); diff --git a/shared/api.ts b/shared/api.ts index ef257f2c8..2ebe5d3d9 100644 --- a/shared/api.ts +++ b/shared/api.ts @@ -39,7 +39,7 @@ * @apiUse UserSession * @apiUse InternalError * - * @apiSuccess (Success 302) Redirect to the URL that has been stored during last call to /verify. + * @apiSuccess (Success 302) Redirect to the URL that has been stored during last call to /api/verify. * * @apiDescription Complete U2F registration request. */ @@ -68,7 +68,7 @@ export const SECOND_FACTOR_U2F_REGISTER_REQUEST_GET = "/api/u2f/register_request * @apiUse UserSession * @apiUse InternalError * - * @apiSuccess (Success 302) Redirect to the URL that has been stored during last call to /verify. + * @apiSuccess (Success 302) Redirect to the URL that has been stored during last call to /api/verify. * @apiError (Error 403) {none} error No authentication request has been provided. * * @apiDescription Complete authentication request of the U2F device. @@ -100,7 +100,7 @@ export const SECOND_FACTOR_U2F_SIGN_REQUEST_GET = "/api/u2f/sign_request"; * * @apiParam {String} token TOTP token. * - * @apiSuccess (Success 302) Redirect to the URL that has been stored during last call to /verify. + * @apiSuccess (Success 302) Redirect to the URL that has been stored during last call to /api/verify. * @apiError (Error 401) {none} error TOTP token is invalid. * * @apiDescription Verify TOTP token. The user is authenticated upon success. @@ -270,8 +270,13 @@ export const SECOND_FACTOR_GET = "/secondfactor"; * @apiVersion 1.0.0 * @apiUse UserSession * + * @apiParam {String} redirect Optional parameter set to the url where the user + * is redirected if access is refused. It is mainly used by Traefik that does + * not control the redirection itself. + * * @apiSuccess (Success 204) status The user is authenticated. - * @apiError (Error 401) status The user is not authenticated. + * @apiError (Error 302) redirect The user is redirected if redirect parameter is provided. + * @apiError (Error 401) status The user get an error if access failed * * @apiDescription Verify that the user is authenticated, i.e., the two * factors have been validated. diff --git a/test/features/authelia.feature b/test/features/authelia.feature new file mode 100644 index 000000000..7c7f472db --- /dev/null +++ b/test/features/authelia.feature @@ -0,0 +1,9 @@ +Feature: Generic tests on Authelia endpoints + + Scenario: /api/verify replies with error when redirect parameter is not provided + When I query "https://authelia.example.com:8080/api/verify" + Then I get error code 401 + + Scenario: /api/verify redirects when redirect parameter is provided + When I query "https://authelia.example.com:8080/api/verify?redirect=http://login.example.com:8080" + Then I get redirected to "http://login.example.com:8080" \ No newline at end of file diff --git a/test/features/step_definitions/authelia.ts b/test/features/step_definitions/authelia.ts new file mode 100644 index 000000000..da3f9676b --- /dev/null +++ b/test/features/step_definitions/authelia.ts @@ -0,0 +1,51 @@ +import Cucumber = require("cucumber"); +import seleniumWebdriver = require("selenium-webdriver"); +import Assert = require("assert"); +import Request = require("request-promise"); +import Bluebird = require("bluebird"); + +Cucumber.defineSupportCode(function ({ Given, When, Then, Before, After }) { + Before(function () { + this.jar = Request.jar(); + }) + + When("I query {stringInDoubleQuotes}", function (url: string) { + const that = this; + return Request(url, { followRedirect: false }) + .then(function(response) { + console.log(response); + that.response = response; + }) + .catch(function(err: Error) { + that.error = err; + }) + }); + + Then("I get error code 401", function() { + const that = this; + return new Bluebird(function(resolve, reject) { + if(that.error && that.error.statusCode == 401) { + resolve(); + } + else { + if(that.response) + reject(new Error("No error thrown")); + else if(that.error.statusCode != 401) + reject(new Error("Error code != 401")); + } + }); + }); + + Then("I get redirected to {stringInDoubleQuotes}", function(url: string) { + const that = this; + return new Bluebird(function(resolve, reject) { + if(that.error && that.error.statusCode == 302 + && that.error.message.indexOf(url) > -1) { + resolve(); + } + else { + reject(new Error("Not redirected")); + } + }); + }) +}); \ No newline at end of file