Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions PropelAuth.Tests/UserTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,12 @@ private OrgMemberInfo CreateTestOrgMemberInfo(
user_permissions: permissions
);
}

private ClaimsPrincipal CreateEmptyClaimsPrincipal()
{
var identity = new ClaimsIdentity(new List<Claim>(), "TestAuth");
return new ClaimsPrincipal(identity);
}

[Fact]
public void Constructor_ShouldInitializeBasicProperties()
Expand Down Expand Up @@ -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());
}
}
}
63 changes: 60 additions & 3 deletions PropelAuth/PropelAuthExtensions.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -71,17 +75,70 @@ private static RSA ConfigureRsaWithPublicKey(string publicKey)
/// <param name="rsa">The RSA instance configured with the public key.</param>
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<string>() {"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();
}
Expand Down
57 changes: 56 additions & 1 deletion PropelAuth/PropelAuthOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@
/// </summary>
public string ApiKey { get; }

/// <summary>
/// If you are using PropelAuth's OAuth feature, you can specify the OAuth options here.
/// </summary>
public OAuthOptions? OAuthOptions { get; }

#endregion

#region Constructors
Expand All @@ -35,13 +40,63 @@
/// <param name="authUrl">The base URL for the PropelAuth authentication service.</param>
/// <param name="apiKey">The API key used for authenticating requests to PropelAuth.</param>
/// <param name="publicKey">Optional. The public key used for token verification.</param>
public PropelAuthOptions(string authUrl, string apiKey, string? publicKey = null)
/// <param name="oAuthOptions">Optional. The OAuth options if you are using PropelAuth's OAuth feature.</param>
public PropelAuthOptions(string authUrl, string apiKey, string? publicKey = null,
OAuthOptions? oAuthOptions = null)
{
AuthUrl = authUrl;
ApiKey = apiKey;
PublicKey = publicKey;
OAuthOptions = oAuthOptions;
}

#endregion
}

public class OAuthOptions

Check warning on line 56 in PropelAuth/PropelAuthOptions.cs

View workflow job for this annotation

GitHub Actions / Build and Test

Missing XML comment for publicly visible type or member 'OAuthOptions'
{
#region Properties

/// <summary>
/// The client ID for the OAuth application.
/// </summary>
public string ClientId { get; }

/// <summary>
/// The client secret for the OAuth application.
/// </summary>
public string ClientSecret { get; }

/// <summary>
/// The callback path for the OAuth application. Defaults to "/callback"
/// </summary>
public string? CallbackPath { get; }

/// <summary>
/// Whether to allow requests via an authorization header `Bearer {TOKEN}`. Default false.
/// </summary>
public bool? AllowBearerTokenAuth { get; }

#endregion

#region Constructor

/// <summary>
/// Initializes a new instance of the <see cref="OAuthOptions"/> class.
/// </summary>
/// <param name="clientId">The client ID for the OAuth application.</param>
/// <param name="clientSecret">The client secret for the OAuth application.</param>
/// <param name="callbackPath">Optional. The callback path for the OAuth application. Defaults to "/callback"</param>
/// <param name="allowBearerTokenAuth">Optional. Whether to allow requests via an authorization header `Bearer {TOKEN}`. Default false.</param>
public OAuthOptions(string clientId, string clientSecret, string? callbackPath = "/callback", bool? allowBearerTokenAuth = false)
{
ClientId = clientId;
ClientSecret = clientSecret;
CallbackPath = callbackPath;
AllowBearerTokenAuth = allowBearerTokenAuth;
}

#endregion

}
}
26 changes: 19 additions & 7 deletions PropelAuth/User.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,15 @@
#region Properties

// Public properties
public string UserId { get; }

Check warning on line 14 in PropelAuth/User.cs

View workflow job for this annotation

GitHub Actions / Build and Test

Missing XML comment for publicly visible type or member 'User.UserId'
public string Email { get; }

Check warning on line 15 in PropelAuth/User.cs

View workflow job for this annotation

GitHub Actions / Build and Test

Missing XML comment for publicly visible type or member 'User.Email'
public string? FirstName { get; }

Check warning on line 16 in PropelAuth/User.cs

View workflow job for this annotation

GitHub Actions / Build and Test

Missing XML comment for publicly visible type or member 'User.FirstName'
public string? LastName { get; }

Check warning on line 17 in PropelAuth/User.cs

View workflow job for this annotation

GitHub Actions / Build and Test

Missing XML comment for publicly visible type or member 'User.LastName'
public string? Username { get; }

Check warning on line 18 in PropelAuth/User.cs

View workflow job for this annotation

GitHub Actions / Build and Test

Missing XML comment for publicly visible type or member 'User.Username'
public string? LegacyUserId { get; }

Check warning on line 19 in PropelAuth/User.cs

View workflow job for this annotation

GitHub Actions / Build and Test

Missing XML comment for publicly visible type or member 'User.LegacyUserId'
public LoginMethod LoginMethod { get; }

Check warning on line 20 in PropelAuth/User.cs

View workflow job for this annotation

GitHub Actions / Build and Test

Missing XML comment for publicly visible type or member 'User.LoginMethod'
public string? ImpersonatorUserId { get; }

Check warning on line 21 in PropelAuth/User.cs

View workflow job for this annotation

GitHub Actions / Build and Test

Missing XML comment for publicly visible type or member 'User.ImpersonatorUserId'
public Dictionary<string, object>? Properties { get; }

Check warning on line 22 in PropelAuth/User.cs

View workflow job for this annotation

GitHub Actions / Build and Test

Missing XML comment for publicly visible type or member 'User.Properties'

// Private properties
private Dictionary<string, OrgMemberInfo>? OrgIdToOrgMemberInfo { get; set; }
Expand Down Expand Up @@ -175,12 +175,13 @@
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;
}

/// <summary>
/// Processes organization information from claims.
/// </summary>
Expand All @@ -193,7 +194,8 @@

if (orgsClaim.Type == "org_id_to_org_member_info")
{
OrgIdToOrgMemberInfo = JsonConvert.DeserializeObject<Dictionary<string, OrgMemberInfo>>(orgsClaim.Value);
OrgIdToOrgMemberInfo =
JsonConvert.DeserializeObject<Dictionary<string, OrgMemberInfo>>(orgsClaim.Value);
}
else
{
Expand All @@ -215,7 +217,9 @@
private Dictionary<string, object>? ParseUserProperties(ClaimsPrincipal claimsPrincipal)
{
var propertiesClaim = claimsPrincipal.FindFirst("properties");
return propertiesClaim != null ? JsonConvert.DeserializeObject<Dictionary<string, object>>(propertiesClaim.Value) : null;
return propertiesClaim != null
? JsonConvert.DeserializeObject<Dictionary<string, object>>(propertiesClaim.Value)
: null;
}

/// <summary>
Expand Down Expand Up @@ -276,8 +280,8 @@
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);
}
Expand All @@ -293,6 +297,14 @@
/// <summary>
/// Gets a PropelAuth User from a ClaimsPrincipal.
/// </summary>
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);
}
}
}