public IntegratedAuthenticationModule(ILog log, IAuthCookieCreator tokenIssuer, IApiActionResponseCreator responseCreator, IWebPortalConfigurationStore webPortalConfigurationStore) { Get[DirectoryServicesConstants.ChallengePath] = c => { if (Context.CurrentUser == null) { return(responseCreator.Unauthorized(Request)); } var principal = (IOctopusPrincipal)Context.CurrentUser; var authCookies = tokenIssuer.CreateAuthCookies(Context.Request, principal.IdentificationToken, SessionExpiry.TwentyMinutes); var whitelist = webPortalConfigurationStore.GetTrustedRedirectUrls(); Response response; if (Request.Query["redirectTo"].HasValue && Requests.IsLocalUrl(Request.Query["redirectTo"].Value, whitelist)) { var redirectLocation = Request.Query["redirectTo"].Value; response = new RedirectResponse(redirectLocation).WithCookies(authCookies); } else { if (Request.Query["redirectTo"].HasValue) { log.WarnFormat("Prevented potential Open Redirection attack on an NTLM challenge, to the non-local url {0}", Request.Query["redirectTo"].Value); } response = new RedirectResponse(Request.Url.BasePath ?? "/").WithCookies(authCookies); } return(response); }; }
public IntegratedAuthenticationModule(ILog log, IAuthCookieCreator tokenIssuer, IAuthenticationConfigurationStore authenticationConfigurationStore, IUrlEncoder encoder) { Add("GET", DirectoryServicesConstants.ChallengePath, context => { if (context.User == null) { context.Response.StatusCode = 401; return(Task.FromResult(0)); } var principal = (IOctopusPrincipal)context.User; // Decode the state object sent from the client (if there was one) so we can use those hints to build the most appropriate response // If the state can't be interpreted, we will fall back to a safe-by-default behaviour, however: // 1. Deep-links will not work because we don't know where the anonymous request originally wanted to go // 2. Cookies may not have the Secure flag set properly when SSL Offloading is in play LoginState state = null; if (context.Request.Query.TryGetValue("state", out var stateString)) { try { state = JsonConvert.DeserializeObject <LoginState>(stateString); } catch (Exception e) { log.Warn(e, "Invalid login state object passed to the server when setting up the NTLM challenge. Falling back to the default behaviour."); } } // Build the auth cookies to send back with the response var authCookies = tokenIssuer.CreateAuthCookies(principal.IdentificationToken, SessionExpiry.TwentyDays, context.Request.IsHttps, state?.UsingSecureConnection); // If the caller has provided a redirect after successful login, we need to check it is a local address - preventing Open Redirection attacks if (!string.IsNullOrWhiteSpace(state?.RedirectAfterLoginTo)) { var whitelist = authenticationConfigurationStore.GetTrustedRedirectUrls(); if (Requests.IsLocalUrl(state.RedirectAfterLoginTo, whitelist)) { // This is a safe redirect, let's go! context.Response.Redirect(state.RedirectAfterLoginTo); foreach (var cookie in authCookies) { context.Response.WithCookie(cookie); } return(Task.FromResult(0)); } // Just log that we detected a non-local redirect URL, and fall through to the root of the local web site log.WarnFormat( "Prevented potential Open Redirection attack on an NTLM challenge, to the non-local url {0}", state.RedirectAfterLoginTo); } // By default, redirect to the root of the local web site context.Response.Redirect(context.Request.PathBase ?? "/"); foreach (var cookie in authCookies) { context.Response.WithCookie(cookie); } return(Task.FromResult(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}")); }