diff --git a/PropelAuth.Tests/UserTests.cs b/PropelAuth.Tests/UserTests.cs index d575421..50872f6 100644 --- a/PropelAuth.Tests/UserTests.cs +++ b/PropelAuth.Tests/UserTests.cs @@ -91,6 +91,12 @@ private OrgMemberInfo CreateTestOrgMemberInfo( user_permissions: permissions ); } + + private ClaimsPrincipal CreateEmptyClaimsPrincipal() + { + var identity = new ClaimsIdentity(new List(), "TestAuth"); + return new ClaimsPrincipal(identity); + } [Fact] public void Constructor_ShouldInitializeBasicProperties() @@ -413,5 +419,14 @@ public void GetUserProperty_ShouldReturnNull_WhenPropertiesNull() // Act & Assert Assert.Null(user.GetUserProperty("anyProperty")); } + + [Fact] + public void GetUser_EmptyClaimsPrincipal_ShouldReturnNull() + { + // Arrange + var principal = CreateEmptyClaimsPrincipal(); + + Assert.Null(principal.GetUser()); + } } } \ No newline at end of file diff --git a/PropelAuth/PropelAuthExtensions.cs b/PropelAuth/PropelAuthExtensions.cs index cf5a6c1..2b9df80 100644 --- a/PropelAuth/PropelAuthExtensions.cs +++ b/PropelAuth/PropelAuthExtensions.cs @@ -1,5 +1,9 @@ +using System.IdentityModel.Tokens.Jwt; using System.Security.Cryptography; +using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Authentication.OAuth; +using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Microsoft.IdentityModel.Tokens; using Newtonsoft.Json.Linq; @@ -71,17 +75,70 @@ private static RSA ConfigureRsaWithPublicKey(string publicKey) /// The RSA instance configured with the public key. private static void ConfigureAuthentication(IServiceCollection services, PropelAuthOptions options, RSA rsa) { - services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) - .AddJwtBearer(jwtOptions => + var authBuilder = services.AddAuthentication(authOptions => + { + if (options.OAuthOptions != null) + { + authOptions.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme; + authOptions.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme; + authOptions.DefaultChallengeScheme = "PropelAuth"; + } + else + { + authOptions.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; + authOptions.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; + } + }); + + if (options.OAuthOptions == null || options.OAuthOptions.AllowBearerTokenAuth == true) + { + authBuilder.AddJwtBearer(jwtOptions => { jwtOptions.TokenValidationParameters = new TokenValidationParameters { ValidateAudience = false, ValidAlgorithms = new List() {"RS256"}, ValidIssuer = options.AuthUrl, - IssuerSigningKey = new RsaSecurityKey(rsa) + IssuerSigningKey = new RsaSecurityKey(rsa), + ValidateLifetime = true, }; }); + } + else + { + authBuilder + .AddCookie(cookieOptions => + { + cookieOptions.Cookie.SameSite = SameSiteMode.Lax; + cookieOptions.Cookie.HttpOnly = true; + cookieOptions.Cookie.SecurePolicy = CookieSecurePolicy.Always; + cookieOptions.SlidingExpiration = true; + }) + .AddOAuth("PropelAuth", configOptions => + { + configOptions.AuthorizationEndpoint = $"{options.AuthUrl}/propelauth/oauth/authorize"; + configOptions.TokenEndpoint = $"{options.AuthUrl}/propelauth/oauth/token"; + configOptions.UserInformationEndpoint = $"{options.AuthUrl}/propelauth/oauth/userinfo"; + configOptions.ClientId = options.OAuthOptions.ClientId; + configOptions.ClientSecret = options.OAuthOptions.ClientSecret; + configOptions.CallbackPath = options.OAuthOptions.CallbackPath; + configOptions.SaveTokens = true; + configOptions.Events = new OAuthEvents + { + OnCreatingTicket = context => + { + var token = context.AccessToken; + var handler = new JwtSecurityTokenHandler(); + var jwt = handler.ReadJwtToken(token); + foreach (var claim in jwt.Claims) + { + context.Identity?.AddClaim(claim); + } + return Task.CompletedTask; + } + }; + }); + } services.AddAuthorization(); } diff --git a/PropelAuth/PropelAuthOptions.cs b/PropelAuth/PropelAuthOptions.cs index 16e1076..9edc63b 100644 --- a/PropelAuth/PropelAuthOptions.cs +++ b/PropelAuth/PropelAuthOptions.cs @@ -25,6 +25,11 @@ public class PropelAuthOptions /// public string ApiKey { get; } + /// + /// If you are using PropelAuth's OAuth feature, you can specify the OAuth options here. + /// + public OAuthOptions? OAuthOptions { get; } + #endregion #region Constructors @@ -35,13 +40,63 @@ public class PropelAuthOptions /// The base URL for the PropelAuth authentication service. /// The API key used for authenticating requests to PropelAuth. /// Optional. The public key used for token verification. - public PropelAuthOptions(string authUrl, string apiKey, string? publicKey = null) + /// Optional. The OAuth options if you are using PropelAuth's OAuth feature. + public PropelAuthOptions(string authUrl, string apiKey, string? publicKey = null, + OAuthOptions? oAuthOptions = null) { AuthUrl = authUrl; ApiKey = apiKey; PublicKey = publicKey; + OAuthOptions = oAuthOptions; + } + + #endregion + } + + public class OAuthOptions + { + #region Properties + + /// + /// The client ID for the OAuth application. + /// + public string ClientId { get; } + + /// + /// The client secret for the OAuth application. + /// + public string ClientSecret { get; } + + /// + /// The callback path for the OAuth application. Defaults to "/callback" + /// + public string? CallbackPath { get; } + + /// + /// Whether to allow requests via an authorization header `Bearer {TOKEN}`. Default false. + /// + public bool? AllowBearerTokenAuth { get; } + + #endregion + + #region Constructor + + /// + /// Initializes a new instance of the class. + /// + /// The client ID for the OAuth application. + /// The client secret for the OAuth application. + /// Optional. The callback path for the OAuth application. Defaults to "/callback" + /// Optional. Whether to allow requests via an authorization header `Bearer {TOKEN}`. Default false. + public OAuthOptions(string clientId, string clientSecret, string? callbackPath = "/callback", bool? allowBearerTokenAuth = false) + { + ClientId = clientId; + ClientSecret = clientSecret; + CallbackPath = callbackPath; + AllowBearerTokenAuth = allowBearerTokenAuth; } #endregion + } } \ No newline at end of file diff --git a/PropelAuth/User.cs b/PropelAuth/User.cs index 09d5842..6c6291e 100644 --- a/PropelAuth/User.cs +++ b/PropelAuth/User.cs @@ -175,12 +175,13 @@ private string ExtractEmail(ClaimsPrincipal claimsPrincipal) string? email = claimsPrincipal.FindFirstValue(ClaimTypes.Email); if (string.IsNullOrEmpty(email)) { - throw new ArgumentException($"Required claim '{ClaimTypes.Email}' is missing or empty", nameof(claimsPrincipal)); + throw new ArgumentException($"Required claim '{ClaimTypes.Email}' is missing or empty", + nameof(claimsPrincipal)); } return email; } - + /// /// Processes organization information from claims. /// @@ -193,7 +194,8 @@ private void ProcessOrgInformation(ClaimsPrincipal claimsPrincipal) if (orgsClaim.Type == "org_id_to_org_member_info") { - OrgIdToOrgMemberInfo = JsonConvert.DeserializeObject>(orgsClaim.Value); + OrgIdToOrgMemberInfo = + JsonConvert.DeserializeObject>(orgsClaim.Value); } else { @@ -215,7 +217,9 @@ private void ProcessOrgInformation(ClaimsPrincipal claimsPrincipal) private Dictionary? ParseUserProperties(ClaimsPrincipal claimsPrincipal) { var propertiesClaim = claimsPrincipal.FindFirst("properties"); - return propertiesClaim != null ? JsonConvert.DeserializeObject>(propertiesClaim.Value) : null; + return propertiesClaim != null + ? JsonConvert.DeserializeObject>(propertiesClaim.Value) + : null; } /// @@ -276,8 +280,8 @@ private static LoginMethod CreateSamlSsoLoginMethod(Dictionary l var samlProvider = loginMethodData.TryGetValue("provider", out var samlProviderValue) ? samlProviderValue : "unknown"; - var orgId = loginMethodData.TryGetValue("org_id", out var orgIdValue) - ? orgIdValue + var orgId = loginMethodData.TryGetValue("org_id", out var orgIdValue) + ? orgIdValue : "unknown"; return LoginMethod.SamlSso(samlProvider, orgId); } @@ -293,6 +297,14 @@ public static class ClaimsPrincipalExtensions /// /// Gets a PropelAuth User from a ClaimsPrincipal. /// - public static User GetUser(this ClaimsPrincipal claimsPrincipal) => new(claimsPrincipal); + public static User? GetUser(this ClaimsPrincipal claimsPrincipal) + { + if (claimsPrincipal.FindFirstValue("user_id") == null) + { + return null; + } + + return new User(claimsPrincipal); + } } } \ No newline at end of file