public async Task <Response> ExecuteAsync(NancyContext context, IResponseFormatter response)
        {
            if (ConfigurationStore.GetIsEnabled() == false)
            {
                log.Warn($"{ConfigurationStore.ConfigurationSettingsName} user authentication API was called while the provider was disabled.");
                return(ResponseCreator.BadRequest(new string[] { "This authentication provider is disabled." }));
            }

            var model = modelBinder.Bind <LoginRedirectLinkRequestModel>(context);

            var state = model.RedirectAfterLoginTo;

            if (string.IsNullOrWhiteSpace(state))
            {
                state = "/";
            }

            var whitelist = webPortalConfigurationStore.GetTrustedRedirectUrls();

            if (!Requests.IsLocalUrl(state, whitelist))
            {
                log.WarnFormat("Prevented potential Open Redirection attack on an authentication request, to the non-local url {0}", state);
                return(ResponseCreator.BadRequest("Request not allowed, due to potential Open Redirection attack"));
            }

            var nonce = Nonce.GenerateUrlSafeNonce();

            try
            {
                var issuer       = ConfigurationStore.GetIssuer();
                var issuerConfig = await identityProviderConfigDiscoverer.GetConfigurationAsync(issuer);

                var url = urlBuilder.Build(model.ApiAbsUrl, issuerConfig, nonce, state);

                return(ResponseCreator.AsOctopusJson(response, new LoginRedirectLinkResponseModel {
                    ExternalAuthenticationUrl = url
                })
                       .WithCookie(new NancyCookie("s", State.Protect(state), true, false, DateTime.UtcNow.AddMinutes(20)))
                       .WithCookie(new NancyCookie("n", Nonce.Protect(nonce), true, false, DateTime.UtcNow.AddMinutes(20))));
            }
            catch (Exception ex)
            {
                log.Error(ex);
                return(response.AsRedirect($"{state}?error=Login failed. Please see the Octopus Server logs for more details."));
            }
        }
        public async Task <Response> ExecuteAsync(NancyContext context, IResponseFormatter response)
        {
            if (ConfigurationStore.GetIsEnabled() == false)
            {
                log.Warn($"{ConfigurationStore.ConfigurationSettingsName} user authentication API was called while the provider was disabled.");
                return(ResponseCreator.BadRequest(new string[] { "This authentication provider is disabled." }));
            }

            if (context.Request.Url.SiteBase.StartsWith("https://", StringComparison.OrdinalIgnoreCase) == false)
            {
                log.Warn($"{ConfigurationStore.ConfigurationSettingsName} user authentication API was called without using https.");
            }

            var postLoginRedirectTo = context.Request.Query["redirectTo"];
            var state = "~/app";

            if (string.IsNullOrWhiteSpace(postLoginRedirectTo) == false)
            {
                state = postLoginRedirectTo;
            }
            var nonce = Nonce.Generate();

            try
            {
                var issuer       = ConfigurationStore.GetIssuer();
                var issuerConfig = await identityProviderConfigDiscoverer.GetConfigurationAsync(issuer);

                var url = urlBuilder.Build(context.Request.Url.SiteBase, issuerConfig, nonce, state);

                return(response.AsRedirect(url)
                       .WithCookie(new NancyCookie("s", State.Protect(state), true, false, DateTime.UtcNow.AddMinutes(20)))
                       .WithCookie(new NancyCookie("n", Nonce.Protect(nonce), true, false, DateTime.UtcNow.AddMinutes(20))));
            }
            catch (Exception ex)
            {
                log.Error(ex);
                return(response.AsRedirect($"{state}?error=Login failed. Please see the Octopus Server logs for more details."));
            }
        }
Пример #3
0
        public async Task <Response> ExecuteAsync(NancyContext context, IResponseFormatter response)
        {
            // Step 1: Try and get all of the details from the request making sure there are no errors passed back from the external identity provider
            string stateFromRequest;
            var    principalContainer = await authTokenHandler.GetPrincipalAsync(context.Request, out stateFromRequest);

            var principal = principalContainer.principal;

            if (principal == null || !string.IsNullOrEmpty(principalContainer.error))
            {
                return(BadRequest($"The response from the external identity provider contained an error: {principalContainer.error}"));
            }

            // Step 2: Validate the state object we passed wasn't tampered with
            const string stateDescription  = "As a security precaution, Octopus ensures the state object returned from the external identity provider matches what it expected.";
            var          expectedStateHash = string.Empty;

            if (context.Request.Cookies.ContainsKey("s"))
            {
                expectedStateHash = HttpUtility.UrlDecode(context.Request.Cookies["s"]);
            }
            if (string.IsNullOrWhiteSpace(expectedStateHash))
            {
                return(BadRequest($"User login failed: Missing State Hash Cookie. {stateDescription} In this case the Cookie containing the SHA256 hash of the state object is missing from the request."));
            }

            var stateFromRequestHash = State.Protect(stateFromRequest);

            if (stateFromRequestHash != expectedStateHash)
            {
                return(BadRequest($"User login failed: Tampered State. {stateDescription} In this case the state object looks like it has been tampered with. The state object is '{stateFromRequest}'. The SHA256 hash of the state was expected to be '{expectedStateHash}' but was '{stateFromRequestHash}'."));
            }

            // Step 3: Validate the nonce is as we expected to prevent replay attacks
            const string nonceDescription = "As a security precaution to prevent replay attacks, Octopus ensures the nonce returned in the claims from the external identity provider matches what it expected.";

            var expectedNonceHash = string.Empty;

            if (context.Request.Cookies.ContainsKey("n"))
            {
                expectedNonceHash = HttpUtility.UrlDecode(context.Request.Cookies["n"]);
            }

            if (string.IsNullOrWhiteSpace(expectedNonceHash))
            {
                return(BadRequest($"User login failed: Missing Nonce Hash Cookie. {nonceDescription} In this case the Cookie containing the SHA256 hash of the nonce is missing from the request."));
            }

            var nonceFromClaims = principal.Claims.FirstOrDefault(c => c.Type == "nonce");

            if (nonceFromClaims == null)
            {
                return(BadRequest($"User login failed: Missing Nonce Claim. {nonceDescription} In this case the 'nonce' claim is missing from the security token."));
            }

            var nonceFromClaimsHash = Nonce.Protect(nonceFromClaims.Value);

            if (nonceFromClaimsHash != expectedNonceHash)
            {
                return(BadRequest($"User login failed: Tampered Nonce. {nonceDescription} In this case the nonce looks like it has been tampered with or reused. The nonce is '{nonceFromClaims}'. The SHA256 hash of the state was expected to be '{expectedNonceHash}' but was '{nonceFromClaimsHash}'."));
            }

            // Step 4: Now the integrity of the request has been validated we can figure out which Octopus User this represents
            var authenticationCandidate = principalToUserResourceMapper.MapToUserResource(principal);

            // Step 4a: Check if this authentication attempt is already being banned
            var action = loginTracker.BeforeAttempt(authenticationCandidate.Username, context.Request.UserHostAddress);

            if (action == InvalidLoginAction.Ban)
            {
                return(BadRequest("You have had too many failed login attempts in a short period of time. Please try again later."));
            }

            // Step 4b: Try to get or create a the Octopus User this external identity represents
            var userResult = GetOrCreateUser(authenticationCandidate, principal);

            if (userResult.Succeeded)
            {
                loginTracker.RecordSucess(authenticationCandidate.Username, context.Request.UserHostAddress);

                var authCookies = authCookieCreator.CreateAuthCookies(context.Request, userResult.User.IdentificationToken, SessionExpiry.TwentyDays);

                return(RedirectResponse(response, stateFromRequest)
                       .WithCookies(authCookies)
                       .WithHeader("Expires", DateTime.UtcNow.AddYears(1).ToString("R", DateTimeFormatInfo.InvariantInfo)));
            }

            // Step 5: Handle other types of failures
            loginTracker.RecordFailure(authenticationCandidate.Username, context.Request.UserHostAddress);

            // Step 5a: Slow this potential attacker down a bit since they seem to keep failing
            if (action == InvalidLoginAction.Slow)
            {
                sleep.For(1000);
            }

            if (!userResult.User.IsActive)
            {
                return(BadRequest($"The Octopus User Account '{authenticationCandidate.Username}' has been disabled by an Administrator. If you believe this to be a mistake, please contact your Octopus Administrator to have your account re-enabled."));
            }

            if (userResult.User.IsService)
            {
                return(BadRequest($"The Octopus User Account '{authenticationCandidate.Username}' is a Service Account, which are prevented from using Octopus interactively. Service Accounts are designed to authorize external systems to access the Octopus API using an API Key."));
            }

            return(BadRequest($"User login failed: {userResult.FailureReason}"));
        }
Пример #4
0
        public async Task <Response> ExecuteAsync(NancyContext context, IResponseFormatter response)
        {
            string state;
            var    principalContainer = await authTokenHandler.GetPrincipalAsync(context.Request, out state);

            var principal = principalContainer.principal;

            if (principal == null || !string.IsNullOrEmpty(principalContainer.error))
            {
                log.Info($"User login failed - {principalContainer.error}");
                return(RedirectResponse(response, $"{context.Request.Form["state"]}?error={principalContainer.error}"));
            }

            var cookieStateHash = string.Empty;

            if (context.Request.Cookies.ContainsKey("s"))
            {
                cookieStateHash = HttpUtility.UrlDecode(context.Request.Cookies["s"]);
            }
            if (State.Protect(state) != cookieStateHash)
            {
                log.Info($"User login failed - invalid state");
                return(RedirectResponse(response, $"{state}?error=Invalid state"));
            }

            var cookieNonceHash = string.Empty;

            if (context.Request.Cookies.ContainsKey("n"))
            {
                cookieNonceHash = HttpUtility.UrlDecode(context.Request.Cookies["n"]);
            }

            var nonce = principal.Claims.FirstOrDefault(c => c.Type == "nonce");

            if (nonce == null || Nonce.Protect(nonce.Value) != cookieNonceHash)
            {
                log.Info("User login failed - invalid nonce");
                return(RedirectResponse(response, $"{state}?error=Invalid nonce"));
            }

            var model = principalToUserHandler.GetUserResource(principal);

            var action = loginTracker.BeforeAttempt(model.Username, context.Request.UserHostAddress);

            if (action == InvalidLoginAction.Ban)
            {
                return(RedirectResponse(response, $"{state}?error=You have had too many failed login attempts in a short period of time. Please try again later."));
            }

            var userResult = GetOrCreateUser(model, principal);

            if (!userResult.Succeeded)
            {
                loginTracker.RecordFailure(model.Username, context.Request.UserHostAddress);

                if (action == InvalidLoginAction.Slow)
                {
                    sleep.For(1000);
                }

                return(RedirectResponse(response, $"{state}?error={userResult.FailureReason}"));
            }

            if (!userResult.User.IsActive || userResult.User.IsService)
            {
                loginTracker.RecordFailure(model.Username, context.Request.UserHostAddress);

                if (action == InvalidLoginAction.Slow)
                {
                    sleep.For(1000);
                }

                return(RedirectResponse(response, $"{state}?error=Invalid username or password"));
            }

            loginTracker.RecordSucess(model.Username, context.Request.UserHostAddress);

            var cookie = authCookieCreator.CreateAuthCookie(context, userResult.User.IdentificationToken, true);

            return(RedirectResponse(response, state)
                   .WithCookie(cookie)
                   .WithHeader("Expires", DateTime.UtcNow.AddYears(1).ToString("R", DateTimeFormatInfo.InvariantInfo)));
        }