Skip to content

Commit

Permalink
- (Security) Fixed issue with Obsidian Login block where Facebook and…
Browse files Browse the repository at this point in the history
… OIDC client authentication were not working. (Fixes #5528)
  • Loading branch information
joshuahenninger committed Aug 22, 2023
1 parent 6dae35c commit 5f22257
Show file tree
Hide file tree
Showing 3 changed files with 132 additions and 29 deletions.
22 changes: 13 additions & 9 deletions Rock.Blocks/Security/Login.cs
Expand Up @@ -626,7 +626,7 @@ public BlockActionResult RemoteLoginStart( RemoteLoginStartRequestBag bag )
return ActionBadRequest( "Please try a different authentication method" );
}

var loginUrl = externalRedirectAuthentication.GenerateExternalLoginUrl( GetCurrentPageUrl(), GetRedirectUrlAfterLogin() );
var loginUrl = externalRedirectAuthentication.GenerateExternalLoginUrl( GetRedirectUri(), GetRedirectUrlAfterLogin() );

if ( loginUrl == null )
{
Expand Down Expand Up @@ -733,13 +733,12 @@ private IEnumerable<NamedComponent<AuthenticationComponent>> GetAuthenticationCo
}

/// <summary>
/// Gets the current page URL.
/// Gets the redirect URI that can be used by external authentication components to complete authentication.
/// </summary>
/// <returns>The current page URL.</returns>
private string GetCurrentPageUrl()
private string GetRedirectUri()
{
var rootUrl = this.RequestContext.RootUrlPath?.TrimEnd( '/' );
return $"{rootUrl}/page/{PageCache.Id}";
var uri = this.RequestContext.RequestUri;
return uri.Scheme + "://" + uri.GetComponents( UriComponents.HostAndPort, UriFormat.UriEscaped ).EnsureTrailingForwardslash() + $"page/{PageCache.Id}";
}

/// <summary>
Expand Down Expand Up @@ -868,7 +867,12 @@ private string GetRedirectUrlAfterLogin( string thirdPartyReturnUrl = null )
return returnUrl;
}

return thirdPartyReturnUrl;
if ( thirdPartyReturnUrl.IsNotNullOrWhiteSpace() )
{
return thirdPartyReturnUrl;
}

return "/";
}

/// <summary>
Expand Down Expand Up @@ -1003,7 +1007,7 @@ private bool IsUserLockedOut( UserLogin userLogin, out string lockedOutMessage )
/// <param name="externalAuthProviders">The external authentication providers.</param>
private void LogInWithExternalAuthProviderIfNeeded( LoginInitializationBox box, List<NamedComponent<AuthenticationComponent>> externalAuthProviders )
{
var redirectUrl = GetCurrentPageUrl();
var redirectUrl = GetRedirectUri();

foreach ( var authProvider in externalAuthProviders.Select( c => c.Component ) )
{
Expand Down Expand Up @@ -1087,7 +1091,7 @@ private void RedirectToSingleExternalAuthProviderIfNeeded( LoginInitializationBo
return;
}

var authLoginUri = externalRedirectAuthentication.GenerateExternalLoginUrl( GetCurrentPageUrl(), GetRedirectUrlAfterLogin() ).AbsoluteUri;
var authLoginUri = externalRedirectAuthentication.GenerateExternalLoginUrl( GetRedirectUri(), GetRedirectUrlAfterLogin() ).AbsoluteUri;

if ( authLoginUri.IsNotNullOrWhiteSpace() )
{
Expand Down
7 changes: 4 additions & 3 deletions Rock.JavaScript.Obsidian.Blocks/src/Security/login.obs
Expand Up @@ -396,12 +396,13 @@

onMounted(() => {
// Redirect since already authenticated.
if (config.shouldRedirect && config.redirectUrl) {
navigate(config.redirectUrl);
if (config.shouldRedirect) {
// If the redirect URL is not set, then redirect to the default route.
navigate(config.redirectUrl ? config.redirectUrl : "/");
}
});

removeCurrentUrlQueryParams("State", "Code", "IsPasswordless");
removeCurrentUrlQueryParams("State", "Code", "IsPasswordless", "state", "code", "scope", "authuser", "prompt");

onConfigurationValuesChanged(useReloadBlock());
</script>
132 changes: 115 additions & 17 deletions Rock/Security/Authentication/OidcClient.cs
Expand Up @@ -199,7 +199,7 @@ public override bool Authenticate( HttpRequest request, out string userName, out
{
var options = new ExternalRedirectAuthenticationOptions
{
RedirectUrl = GetRedirectUrl( request ),
RedirectUrl = GetRedirectUrl(),
Parameters = request.QueryString.ToSimpleQueryStringDictionary()
};

Expand Down Expand Up @@ -244,7 +244,7 @@ public override string EncodePassword( UserLogin user, string password )
/// <returns></returns>
public override Uri GenerateLoginUrl( HttpRequest request )
{
return GenerateExternalLoginUrl( GetRedirectUrl( request ), request.QueryString[PageParameterKey.ReturnUrl] );
return GenerateExternalLoginUrl( GetRedirectUrl(), request.QueryString[PageParameterKey.ReturnUrl] );
}

/// <summary>
Expand Down Expand Up @@ -278,7 +278,7 @@ public override void SetPassword( UserLogin user, string password )
throw new NotImplementedException();
}

private string GetRedirectUrl( HttpRequest request )
private string GetRedirectUrl()
{
return GetAttributeValue( AttributeKey.RedirectUri );
}
Expand Down Expand Up @@ -687,6 +687,7 @@ private void UpdatePersonBirthday( Person person, string birthDate )
person.BirthDay = dateParts[2].AsIntegerOrNull();
}
}

#region IExternalRedirectAuthentication Implementation

/// <inheritdoc/>
Expand All @@ -695,41 +696,49 @@ public ExternalRedirectAuthenticationResult Authenticate( ExternalRedirectAuthen
var result = new ExternalRedirectAuthenticationResult
{
UserName = string.Empty,
ReturnUrl = HttpContext.Current.Session[SessionKey.ReturnUrl].ToStringSafe()
ReturnUrl = GetRequestCookieValue( SessionKey.ReturnUrl )
};

var code = request.Parameters.GetValueOrNull( PageParameterKey.Code );
var state = request.Parameters.GetValueOrNull( PageParameterKey.State );
var validState = HttpContext.Current.Session[SessionKey.State].ToStringSafe();

var validState = GetRequestCookieValue( SessionKey.State );

if ( validState.IsNullOrWhiteSpace() || !state.Equals( validState ) )
{
// Clear the OAuth cookies to require a new OAuth session.
DeleteResponseCookie( SessionKey.Nonce );
DeleteResponseCookie( SessionKey.ReturnUrl );
DeleteResponseCookie( SessionKey.State );
throw new Exception( "State is invalid." );
}

try
{
var client = new TokenClient( GetTokenUrl(), GetAttributeValue( AttributeKey.ApplicationId ), GetAttributeValue( AttributeKey.ApplicationSecret ) );
var response = client.RequestAuthorizationCodeAsync( code, request.RedirectUrl ).GetAwaiter().GetResult();
var response = client.RequestAuthorizationCodeAsync( code, GetAttributeValue( AttributeKey.RedirectUri ) ).GetAwaiter().GetResult();

if ( response.IsError )
{
throw new Exception( response.Error );
}

var nonce = HttpContext.Current.Session[SessionKey.Nonce].ToStringSafe();
var nonce = GetRequestCookieValue( SessionKey.Nonce );
var idToken = GetValidatedIdToken( response.IdentityToken, nonce );
result.UserName = HandleOidcUserAddUpdate( idToken, response.AccessToken );
result.IsAuthenticated = !string.IsNullOrWhiteSpace( result.UserName );

HttpContext.Current.Session[SessionKey.Nonce] = string.Empty;
HttpContext.Current.Session[SessionKey.ReturnUrl] = string.Empty;
HttpContext.Current.Session[SessionKey.State] = string.Empty;
}
catch ( Exception ex )
{
ExceptionLogService.LogException( ex, HttpContext.Current );
}
finally
{
// Clear the OAuth cookies to require a new OAuth session.
DeleteResponseCookie( SessionKey.Nonce );
DeleteResponseCookie( SessionKey.ReturnUrl );
DeleteResponseCookie( SessionKey.State );
}

return result;
}
Expand All @@ -740,18 +749,107 @@ public Uri GenerateExternalLoginUrl( string externalProviderReturnUrl, string su
var nonce = EncodeBcrypt( System.Guid.NewGuid().ToString() );
var state = EncodeBcrypt( System.Guid.NewGuid().ToString() );

HttpContext.Current.Session[SessionKey.Nonce] = nonce;
HttpContext.Current.Session[SessionKey.State] = state;
HttpContext.Current.Session[SessionKey.ReturnUrl] = successfulAuthenticationRedirectUrl;
SetResponseCookieValue( SessionKey.State, state );
SetResponseCookieValue( SessionKey.Nonce, nonce );
SetResponseCookieValue( SessionKey.ReturnUrl, successfulAuthenticationRedirectUrl );

return new Uri( GetLoginUrl( externalProviderReturnUrl, nonce, state ) );
// Use the OIDC RedirectUri attribute instead of the provided external provider return URL.
return new Uri( GetLoginUrl( GetAttributeValue( AttributeKey.RedirectUri ), nonce, state ) );
}

/// <inheritdoc/>
public bool IsReturningFromExternalAuthentication( IDictionary<string, string> parameters )
{
return !string.IsNullOrWhiteSpace( parameters.GetValueOrNull( PageParameterKey.Code ) )
&& !string.IsNullOrWhiteSpace( parameters.GetValueOrNull( PageParameterKey.State ) );
var isReturningFromOAuth = parameters.GetValueOrNull( PageParameterKey.Code ).IsNotNullOrWhiteSpace()
&& parameters.GetValueOrNull( PageParameterKey.State ).IsNotNullOrWhiteSpace()
&& GetRequestCookieValue( SessionKey.State ).IsNotNullOrWhiteSpace();

if ( !isReturningFromOAuth )
{
// If not performing the OIDC process,
// then ensure the OIDC cookies are cleared.
DeleteResponseCookie( SessionKey.Nonce );
DeleteResponseCookie( SessionKey.ReturnUrl );
DeleteResponseCookie( SessionKey.State );
}

return isReturningFromOAuth;
}

#endregion

#region Private Methods

/// <summary>
/// Deletes a cookie from the response.
/// </summary>
/// <param name="cookieName">The cookie name.</param>
private void DeleteResponseCookie( string cookieName )
{
// To delete a cookie, the value must be cleared
// and the expiration date should be a past date.

// Create a cookie instance with a blank value.
var cookie = GetCookieInstance( cookieName, string.Empty );

// Any past date will work to expire a client cookie (except for DateTime.MinValue).
// Using a specific date here instead of a relative date to the current date of the server/client.
cookie.Expires = new DateTime( 1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc );

HttpContext.Current.Response.Cookies.Set( cookie );
}

/// <summary>
/// Creates and returns a new cookie instance with the flags required for the OIDC process.
/// </summary>
/// <param name="cookieName">The cookie name.</param>
/// <param name="cookieValue">The cookie value.</param>
/// <returns>A new cookie instance.</returns>
private HttpCookie GetCookieInstance(string cookieName, string cookieValue)
{
return new HttpCookie( cookieName )
{
// Prevent client-side JS from inspecting the cookie.
HttpOnly = true,

// Use SameSite=Lax so the cookie is sent in the OAuth redirect back to Rock.
SameSite = SameSiteMode.Lax,

// Only add the Secure option if the request site is using HTTPS.
Secure = HttpContext.Current.Request.IsSecureConnection
|| string.Equals( HttpContext.Current.Request.UrlProxySafe().Scheme, "https", StringComparison.OrdinalIgnoreCase ),

// Encrypt the cookie value as an extra layer of security.
Value = Encryption.EncryptString( cookieValue ),
};
}

/// <summary>
/// Gets a request cookie value or <c>""</c> if not found.
/// </summary>
/// <param name="cookieName">The cookie name.</param>
/// <returns>The cookie value.</returns>
private string GetRequestCookieValue( string cookieName )
{
var cookie = HttpContext.Current.Request.Cookies[cookieName];

if ( cookie == null )
{
return string.Empty;
}

return Encryption.DecryptString( cookie?.Value ).ToStringSafe();
}

/// <summary>
/// Sets a response cookie value.
/// </summary>
/// <param name="cookieName">The cookie name.</param>
/// <param name="cookieValue">The cookie value.</param>
private void SetResponseCookieValue( string cookieName, string cookieValue )
{
var cookie = GetCookieInstance( cookieName, cookieValue );
HttpContext.Current.Response.Cookies.Set( cookie );
}

#endregion
Expand Down

0 comments on commit 5f22257

Please sign in to comment.