Skip to content

Commit

Permalink
Support CAS logout for OWIN
Browse files Browse the repository at this point in the history
  • Loading branch information
akunzai committed Apr 14, 2024
1 parent b5cc63d commit f1280bb
Show file tree
Hide file tree
Showing 6 changed files with 130 additions and 10 deletions.
87 changes: 77 additions & 10 deletions src/GSS.Authentication.CAS.Owin/CasAuthenticationHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Owin.Infrastructure;
using Microsoft.Owin.Logging;
using Microsoft.Owin.Security;
Expand All @@ -25,6 +26,11 @@ public CasAuthenticationHandler(ILogger logger)
/// <returns>True if the request was handled, false if the next middleware should be invoked.</returns>
public override async Task<bool> InvokeAsync()
{
if (Options.SignedOutCallbackPath.HasValue && Options.SignedOutCallbackPath == Request.Path)
{
return await HandleSignOutCallbackAsync();
}

if (Options.CallbackPath.HasValue && Options.CallbackPath == Request.Path)
{
return await InvokeReturnPathAsync().ConfigureAwait(false);
Expand All @@ -33,6 +39,17 @@ public override async Task<bool> InvokeAsync()
return false;
}

private Task<bool> HandleSignOutCallbackAsync()
{
var query = Request.Query;
var state = query[State];
var properties = Options.StateDataFormat.Unprotect(state);
Response.Redirect(!string.IsNullOrEmpty(properties?.RedirectUri)
? properties!.RedirectUri
: Options.SignedOutRedirectUri);
return Task.FromResult(true);
}

private async Task<bool> InvokeReturnPathAsync()
{
AuthenticationTicket? ticket = null;
Expand All @@ -41,7 +58,7 @@ private async Task<bool> InvokeReturnPathAsync()
try
{
ticket = await AuthenticateAsync().ConfigureAwait(false);
if (ticket?.Identity == null || !ticket.Identity.IsAuthenticated)
if (ticket?.Identity is not { IsAuthenticated: true })
{
exception = new InvalidOperationException("Invalid return state, unable to redirect.");
properties = ticket?.Properties;
Expand Down Expand Up @@ -89,7 +106,7 @@ private async Task<bool> InvokeReturnPathAsync()

await Options.Provider.RedirectToAuthorizationEndpoint(context).ConfigureAwait(false);

if (context.SignInAsAuthenticationType != null && context.Identity != null)
if (context is { SignInAsAuthenticationType: not null, Identity: not null })
{
var signInIdentity = context.Identity;
if (!string.Equals(signInIdentity.AuthenticationType, context.SignInAsAuthenticationType,
Expand All @@ -102,7 +119,7 @@ private async Task<bool> InvokeReturnPathAsync()
Context.Authentication.SignIn(context.Properties, signInIdentity);
}

if (!context.IsRequestCompleted && context.RedirectUri != null)
if (context is { IsRequestCompleted: false, RedirectUri: not null })
{
if (context.Identity == null)
{
Expand Down Expand Up @@ -150,7 +167,7 @@ protected override async Task<AuthenticationTicket> AuthenticateCoreAsync()
throw new InvalidOperationException("Missing ticket parameter from query");
}

var service = BuildReturnTo(state);
var service = QueryHelpers.AddQueryString(BuildRedirectUri(properties.RedirectUri ?? "/"), State, state);
var principal = await Options.ServiceTicketValidator.ValidateAsync(ticket, service, Request.CallCancelled)
.ConfigureAwait(false);

Expand All @@ -176,6 +193,43 @@ protected override async Task<AuthenticationTicket> AuthenticateCoreAsync()
return new AuthenticationTicket(context.Identity, context.Properties);
}

/// <summary>
/// Handles SignOut
/// </summary>
/// <returns></returns>
protected override async Task ApplyResponseGrantAsync()
{
var signOut = Helper.LookupSignOut(Options.AuthenticationType, Options.AuthenticationMode);
if (signOut != null)
{
AuthenticationTicket? ticket = null;
var redirectContext = new CasRedirectContext(Context, ticket)
{
RedirectUri = signOut.Properties.RedirectUri
};
await Options.Provider.RedirectToIdentityProviderForSignOut(redirectContext).ConfigureAwait(false);
if (redirectContext.Handled)
{
return;
}

var properties = new AuthenticationProperties { RedirectUri = Options.SignedOutRedirectUri };
if (!string.IsNullOrWhiteSpace(signOut.Properties.RedirectUri))
{
properties.RedirectUri = signOut.Properties.RedirectUri;
}

var returnTo = QueryHelpers.AddQueryString(
BuildRedirectUriIfRelative(Options.SignedOutCallbackPath.Value), State,
Options.StateDataFormat.Protect(properties));
var logoutUrl = new UriBuilder(Options.CasServerUrlBase);
logoutUrl.Path += Constants.Paths.Logout;
var redirectUri =
QueryHelpers.AddQueryString(logoutUrl.Uri.AbsoluteUri, Constants.Parameters.Service, returnTo);
Response.Redirect(redirectUri);
}
}

/// <summary>
/// Handles SignIn
/// </summary>
Expand All @@ -202,7 +256,8 @@ protected override Task ApplyResponseChallengeAsync()
// Anti-CSRF
GenerateCorrelationId(Options.CookieManager, state);

var returnTo = BuildReturnTo(Options.StateDataFormat.Protect(state));
var returnTo = QueryHelpers.AddQueryString(BuildRedirectUri(Options.CallbackPath.Value), State,
Options.StateDataFormat.Protect(state));

var authorizationEndpoint =
$"{Options.CasServerUrlBase}/login?service={Uri.EscapeDataString(returnTo)}";
Expand All @@ -213,15 +268,27 @@ protected override Task ApplyResponseChallengeAsync()
return Task.CompletedTask;
}

private string BuildReturnTo(string? state)
private string BuildRedirectUri(string path)
{
var baseUrl = Options.ServiceUrlBase?.IsAbsoluteUri == true
? Options.ServiceUrlBase.AbsoluteUri.TrimEnd('/')
: $"{Request.Scheme}://{Request.Host}{RequestPathBase}";
return
state == null || string.IsNullOrWhiteSpace(state)
? $"{baseUrl}{Options.CallbackPath}"
: $"{baseUrl}{Options.CallbackPath}?state={Uri.EscapeDataString(state)}";
return $"{baseUrl}{path}";
}

/// <summary>
/// Build a redirect path if the given path is a relative path.
/// </summary>
private string BuildRedirectUriIfRelative(string uriString)
{
if (string.IsNullOrWhiteSpace(uriString))
{
return uriString;
}

return Uri.TryCreate(uriString, UriKind.Absolute, out _)
? uriString
: BuildRedirectUri(uriString);
}
}
}
14 changes: 14 additions & 0 deletions src/GSS.Authentication.CAS.Owin/CasAuthenticationOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ public CasAuthenticationOptions() : base(CasDefaults.AuthenticationType)
{
Caption = CasDefaults.AuthenticationType;
CallbackPath = new PathString("/signin-cas");
SignedOutCallbackPath = new PathString("/signout-callback-cas");
AuthenticationMode = AuthenticationMode.Passive;
BackchannelTimeout = TimeSpan.FromSeconds(60);
CookieManager = new CookieManager();
Expand Down Expand Up @@ -81,6 +82,19 @@ public string Caption
/// Default will automatic generated by OWIN request
/// </summary>
public Uri? ServiceUrlBase { get; set; }

/// <summary>
/// The request path within the application's base path where the user agent will be returned after sign out from the CAS server.
/// See service from https://apereo.github.io/cas/6.6.x/protocol/CAS-Protocol-Specification.html#231-parameters
/// </summary>
public PathString SignedOutCallbackPath { get; set; }

/// <summary>
/// The uri where the user agent will be redirected to after application is signed out from the identity provider.
/// The redirect will happen after the SignedOutCallbackPath is invoked.
/// </summary>
/// <remarks>This URI can be out of the application's domain. By default it points to the root.</remarks>
public string SignedOutRedirectUri { get; set; } = "/";

/// <summary>
/// Gets or sets the <see cref="IServiceTicketValidator"/> used to validate service ticket.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,22 @@ public class CasAuthenticationProvider : ICasAuthenticationProvider
context.Response.Redirect(context.RedirectUri);
return Task.CompletedTask;
};

/// <summary>
/// Invoked before redirecting to the identity provider to sign out.
/// </summary>
public Func<CasRedirectContext, Task> OnRedirectToIdentityProviderForSignOut { get; set; } = _ => Task.CompletedTask;

public Func<CasRemoteFailureContext, Task> OnRemoteFailure { get; set; } = _ => Task.CompletedTask;

public virtual Task CreatingTicket(CasCreatingTicketContext context) => OnCreatingTicket(context);

public virtual Task RedirectToAuthorizationEndpoint(CasRedirectToAuthorizationEndpointContext context) => OnRedirectToAuthorizationEndpoint(context);

/// <summary>
/// Invoked before redirecting to the identity provider to sign out.
/// </summary>
public virtual Task RedirectToIdentityProviderForSignOut(CasRedirectContext context) => OnRedirectToIdentityProviderForSignOut(context);

public Task RemoteFailure(CasRemoteFailureContext context) => OnRemoteFailure(context);
}
Expand Down
23 changes: 23 additions & 0 deletions src/GSS.Authentication.CAS.Owin/Events/CasRedirectContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using Microsoft.Owin;
using Microsoft.Owin.Security;
using Microsoft.Owin.Security.Provider;

// ReSharper disable once CheckNamespace
namespace GSS.Authentication.CAS.Owin;

public class CasRedirectContext : ReturnEndpointContext
{
public CasRedirectContext(IOwinContext context, AuthenticationTicket? ticket) : base(context, ticket)
{
}

/// <summary>
/// If true, will skip any default logic for this redirect.
/// </summary>
public bool Handled { get; private set; }

/// <summary>
/// Skips any default logic for this redirect.
/// </summary>
public void HandleResponse() => Handled = true;
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ public interface ICasAuthenticationProvider
/// <param name="context">Contains redirect URI and <see cref="AuthenticationProperties"/> of the challenge.</param>
/// <returns></returns>
Task RedirectToAuthorizationEndpoint(CasRedirectToAuthorizationEndpointContext context);

/// <summary>
/// Invoked before redirecting to the identity provider to sign out.
/// </summary>
Task RedirectToIdentityProviderForSignOut(CasRedirectContext context);

/// <summary>
/// Invoked when there is a remote failure.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.WebUtilities" />
<PackageReference Include="Microsoft.Owin.Security.Cookies" />
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" />
<PackageReference Include="Microsoft.Extensions.Options" />
Expand Down

0 comments on commit f1280bb

Please sign in to comment.