diff --git a/README.md b/README.md old mode 100644 new mode 100755 index bdd151b..eb2d3ef --- a/README.md +++ b/README.md @@ -45,7 +45,18 @@ Authentication can be totally configured adding an _Authentication_ section in t // to validate the API Key. //"ApiKeyValue": "f1I7S5GXa4wQDgLQWgz0", "DefaultUserName": "ApiUser" // Required ApiKeyValue is used - } + }, + "Auth0":{ + // This parameters are taken from https://auth0.com/ and you can use these only + // for test or you can create your personal api easily from dashboard. + "SchemeName": "Auth0", + "Algorithm": "RS256", + "Domain": "dev-a6-vyksc.us.auth0.com", + "Audience": "https://github.com/micheletolve", + "ClientId": "ipSAr24nCse9QIAlpN6nm2sYdarlaVY5", + "ClientSecret": "dr-qxPyLT2O7eDzCdzal9CHAe-V7t-aouZWBsDNCUsCk6r-rOjrVRQtZ9zGL7wCT", + "GrantType": "client_credentials" + } } @@ -55,4 +66,4 @@ The _DefaultScheme_ attribute is used to specify what kind of authentication mus **Contribute** -The project is constantly evolving. Contributions are welcome. Feel free to file issues and pull requests on the repo and we'll address them as we can. +The project is constantly evolving. Contributions are welcome. Feel free to file issues and pull requests on the repo and we'll address them as we can. diff --git a/samples/SimpleAuthentication.WebApi/Controllers/IdentityController.cs b/samples/SimpleAuthentication.WebApi/Controllers/IdentityController.cs index f1fd907..256db90 100644 --- a/samples/SimpleAuthentication.WebApi/Controllers/IdentityController.cs +++ b/samples/SimpleAuthentication.WebApi/Controllers/IdentityController.cs @@ -1,6 +1,9 @@ using System.Net.Mime; using System.Security.Claims; +using System.Text.Json; +using System.Text.Json.Serialization; using Microsoft.AspNetCore.Mvc; +using SimpleAuthentication.Auth0; using SimpleAuthentication.JwtBearer; namespace SimpleAuthentication.WebApi.Controllers; @@ -58,6 +61,25 @@ public ActionResult Refresh(string token, bool validateLifetime = var newToken = jwtBearerService.RefreshToken(token, validateLifetime, expiration); return new LoginResponse(newToken); } + + [HttpPost] + [Route("auth0/login")] + [ProducesResponseType(typeof(LoginResponse), StatusCodes.Status200OK)] + [ProducesDefaultResponseType] + public ActionResult LoginAuth0([FromServices] IAuth0Service auth0Service) + { + // Check for login rights... + + // Add custom claims (optional). + var claims = new List + { + new(ClaimTypes.GivenName, "Marco"), + new(ClaimTypes.Surname, "Minerva") + }; + + var token = auth0Service.ObtainTokenAsync(claims); + return new LoginResponse(token.Result); + } } public record class LoginRequest(string UserName, string Password); diff --git a/samples/SimpleAuthentication.WebApi/Controllers/MeController.cs b/samples/SimpleAuthentication.WebApi/Controllers/MeController.cs index 5bdf36b..67a9c1a 100644 --- a/samples/SimpleAuthentication.WebApi/Controllers/MeController.cs +++ b/samples/SimpleAuthentication.WebApi/Controllers/MeController.cs @@ -16,6 +16,16 @@ public class MeController : ControllerBase [ProducesDefaultResponseType] public ActionResult GetWithBearer() => new User(User.Identity!.Name); + + [Authorize(AuthenticationSchemes = "ApiKey")] + [HttpGet("authorize-apikey")] + public User GetWithApiKey() + => new(User.Identity!.Name); + + [Authorize(AuthenticationSchemes = "Auth0")] + [HttpGet("authorize-auth0")] + public User GetWithAuth0() + => new("Auth0 default user"); } public record class User(string? UserName); \ No newline at end of file diff --git a/samples/SimpleAuthentication.WebApi/SimpleAuthentication.WebApi.csproj b/samples/SimpleAuthentication.WebApi/SimpleAuthentication.WebApi.csproj index 954285f..6353f5a 100644 --- a/samples/SimpleAuthentication.WebApi/SimpleAuthentication.WebApi.csproj +++ b/samples/SimpleAuthentication.WebApi/SimpleAuthentication.WebApi.csproj @@ -8,6 +8,7 @@ + diff --git a/samples/SimpleAuthentication.WebApi/appsettings.json b/samples/SimpleAuthentication.WebApi/appsettings.json index 6ba9a40..75b76f5 100644 --- a/samples/SimpleAuthentication.WebApi/appsettings.json +++ b/samples/SimpleAuthentication.WebApi/appsettings.json @@ -2,7 +2,7 @@ "Authentication": { "DefaultScheme": "Bearer", // Optional "JwtBearer": { - "SchemeName": "Bearer", // Default Bearer + "SchemeName": "Bearer", // Default Bearer "SecurityKey": "YKgsOiwvDLJe42dyyL3FkhlMAzZZ2Cmr0FTpyLsPE5DA2afd6NbbCV3d5oHDG2rVBaDHH540EUmrzXPPk2LnfanCdERl4apucmu2Ev5oVgN6dGCr8MMxXIIyTaNmmXHSsaONo75UkxQvFtsm9Qsnsz3VxuNzsoqrzqBQdsDvClo1LcrRNNcTdKcvceq1G57PZNxOWFS749wnsqq7r17a9vvinTdYME2umo7DRn8XUiwbdOajCehJfqipIjwbcuoCIrCwwMizKSiidw5KXU7koVvUSV0UH3o4TWHsVBnt5B1os6oPKtCQ63CPqlwHB5Pet4mzA2lhaFROZXbStpigaRJf3J6AOwZurMbo3LhzCpPW6KZwkixMpwCb82ekZvL0tmfQA2LeWDL2esZ9N4N8w8CzxrZt4gyEfywBwsoFohC0ydVznDpwbgCg05ktuczX3FFcsXEErwtY2wu0or0TSrUSnzIrYP26dOOUh4qREPJ7ZnZ5NoQjOMcXkiThdMuy", // Required "Algorithm": "HS256", // Default HS256 "Issuers": [ "issuer" ], // Optional @@ -21,6 +21,15 @@ // to validate the API Key. //"ApiKeyValue": "f1I7S5GXa4wQDgLQWgz0", "DefaultUserName": "ApiUser" // Required when ApiKeyValue is used + }, + "Auth0":{ + // This parameter are getteedtaken from https://auth0.com/ only for test + "SchemeName": "Auth0", + "Algorithm": "RS256", + "Domain": "", + "Audience": "", + "ClientId": "", + "ClientSecret": "" } }, "Logging": { diff --git a/src/SimpleAuthentication/Auth0/Auth0Service.cs b/src/SimpleAuthentication/Auth0/Auth0Service.cs new file mode 100755 index 0000000..e3042d5 --- /dev/null +++ b/src/SimpleAuthentication/Auth0/Auth0Service.cs @@ -0,0 +1,149 @@ +using System.IdentityModel.Tokens.Jwt; +using System.Net.Http.Headers; +using System.Security.Claims; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Protocols; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; +using Microsoft.IdentityModel.Tokens; + + +namespace SimpleAuthentication.Auth0; + +/// +/// The auth0 service. +/// +internal class Auth0Service : IAuth0Service +{ + private readonly Auth0Settings auth0Setting; + private readonly IHttpClientFactory httpClientFactory; + + /// + /// Initializes a new instance of the class. + /// + /// The auth0 setting options. + /// The http client factory. + public Auth0Service(IOptions auth0SettingOptions, IHttpClientFactory httpClientFactory) + { + auth0Setting = auth0SettingOptions.Value; + this.httpClientFactory = httpClientFactory; + } + + /// + /// Obtains the token async. + /// + /// The claims. + /// A Task. + public async Task ObtainTokenAsync(IList? claims = null) + { + claims ??= new List(); + + var jsonObject = new + { + client_id = auth0Setting.ClientId, + client_secret = auth0Setting.ClientSecret, + audience = auth0Setting.Audience, + grant_type = auth0Setting.GrantType + }; + + string json = JsonSerializer.Serialize(value: jsonObject); + PrepareHttpClient(json, out HttpClient client, out StringContent content); + + try + { + HttpResponseMessage httpResponseMessage = await client.PostAsync("/oauth/token", content); + + if (httpResponseMessage.IsSuccessStatusCode) + { + var response = httpResponseMessage.Content.ReadAsStringAsync(); + var token = JsonSerializer.Deserialize(response.Result)!; + + claims.Update(ClaimTypes.Expiration, token.ExpiresIn.ToString()); + claims.Update(ClaimTypes.AuthenticationInstant, DateTime.UtcNow.ToString()); + + return token.Token; + } + + return httpResponseMessage.ReasonPhrase!; + } + catch (HttpRequestException e) + { + throw new HttpRequestException($"Error occurred while sending the request to obtain the Jwt Token from Auth0 provider. Error {e.Message}"); + //return e.Message; + } + } + + #region PrivateMethod + /// + /// Prepares the http client. + /// + /// The json. + /// The client. + /// The content. + private void PrepareHttpClient(string json, out HttpClient client, out StringContent content) + { + var baseUri = new Uri($"https:/{auth0Setting.Domain}"); + content = SetContent(json); + + client = httpClientFactory.CreateClient(auth0Setting.SchemeName); + client.Timeout = TimeSpan.FromSeconds(30); + client.BaseAddress = baseUri; + client.DefaultRequestHeaders.Host = baseUri.Host; + client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + } + + /// + /// Configure the content for an http request + /// + /// The json serialized of the body + /// the content readey for the request + private static StringContent SetContent(string json) + { + if (string.IsNullOrEmpty(json)) + return null; + + StringContent content = new(json, Encoding.UTF8, "application/json"); + content.Headers.ContentLength = json.Length; + return content; + } + #endregion +} + +/// +/// The Auth0TokenResponse class. +/// +public record class Auth0TokenResponse +{ + /// + /// Gets or sets the token. + /// + [JsonPropertyName("access_token")] + public string Token { get; set; } + + /// + /// Gets or sets the expires in. + /// + [JsonPropertyName("expires_in")] + public int ExpiresIn { get; set; } + + /// + /// Gets or sets the type. + /// + [JsonPropertyName("token_type")] + public string Type { get; set; } + + /// + /// Initializes a new instance of the class. + /// + /// The token. + /// The expires in. + /// The type. + public Auth0TokenResponse(string token, int expiresIn, string type) + { + this.Token = token; + this.ExpiresIn = expiresIn; + this.Type = type; + } +} \ No newline at end of file diff --git a/src/SimpleAuthentication/Auth0/Auth0Settings.cs b/src/SimpleAuthentication/Auth0/Auth0Settings.cs new file mode 100644 index 0000000..7e04c79 --- /dev/null +++ b/src/SimpleAuthentication/Auth0/Auth0Settings.cs @@ -0,0 +1,45 @@ +using Microsoft.AspNetCore.Authentication.JwtBearer; + +namespace SimpleAuthentication.Auth0 +{ + /// + /// Options class provides information needed to control Auth0 Authentication handler behavior. + /// + public class Auth0Settings + { + /// + /// Gets or sets The authentication scheme name (Default: Bearer). + /// + public string SchemeName { get; set; } = JwtBearerDefaults.AuthenticationScheme; + + /// + /// Gets or sets the cryptographic algorithm that is used to generate the digital signature (Default: RS256). + /// + public string Algorithm { get; set; } = "RS256"; + + /// + /// Gets or sets the domain. + /// + public string Domain { get; set; } = null!; + + /// + /// Gets or sets the valid audiences that will be used to check against the token's audience. + /// + public string Audience { get; set; } = null!; + + /// + /// Gets or sets the client id. + /// + public string ClientId { get; set; } = null!; + + /// + /// Gets or sets the client secret. + /// + public string ClientSecret { get; set; } = null!; + + /// + /// Gets or sets the grant type. + /// + public string GrantType { get; set; } = "client_credentials"; + } +} \ No newline at end of file diff --git a/src/SimpleAuthentication/Auth0/IAuth0Service.cs b/src/SimpleAuthentication/Auth0/IAuth0Service.cs new file mode 100755 index 0000000..ef2c8db --- /dev/null +++ b/src/SimpleAuthentication/Auth0/IAuth0Service.cs @@ -0,0 +1,17 @@ +using System.Security.Claims; + +namespace SimpleAuthentication.Auth0 +{ + /// + /// Provides methods for Auth0 Bearer generation and validation. + /// + public interface IAuth0Service + { + /// + /// Obtains a bearer token string from Auth0 provider. + /// + /// The claims list. + /// The JWT bearer token. + Task ObtainTokenAsync(IList? claims = null); + } +} \ No newline at end of file diff --git a/src/SimpleAuthentication/SimpleAuthenticationExtensions.cs b/src/SimpleAuthentication/SimpleAuthenticationExtensions.cs index 4ec2fcb..641a4a9 100644 --- a/src/SimpleAuthentication/SimpleAuthenticationExtensions.cs +++ b/src/SimpleAuthentication/SimpleAuthenticationExtensions.cs @@ -10,6 +10,7 @@ using Microsoft.Net.Http.Headers; using Microsoft.OpenApi.Models; using SimpleAuthentication.ApiKey; +using SimpleAuthentication.Auth0; using SimpleAuthentication.JwtBearer; using SimpleAuthentication.Swagger; using Swashbuckle.AspNetCore.SwaggerGen; @@ -64,6 +65,7 @@ public static ISimpleAuthenticationBuilder AddSimpleAuthentication(this Authenti { CheckAddJwtBearer(builder, configuration.GetSection($"{sectionName}:JwtBearer")); CheckAddApiKey(builder, configuration.GetSection($"{sectionName}:ApiKey")); + CheckAddAuth0(builder, configuration.GetSection($"{sectionName}:Auth0")); return new DefaultSimpleAuthenticationBuilder(configuration, builder); @@ -134,6 +136,30 @@ static void CheckAddApiKey(AuthenticationBuilder builder, IConfigurationSection options.DefaultUserName = settings.DefaultUserName; }); } + + static void CheckAddAuth0(AuthenticationBuilder builder, IConfigurationSection section) + { + var auth0Settings = section.Get(); + if (auth0Settings is null) + { + return; + } + + ArgumentNullException.ThrowIfNull(auth0Settings.SchemeName, nameof(Auth0Settings.SchemeName)); + ArgumentNullException.ThrowIfNull(auth0Settings.Domain, nameof(Auth0Settings.Domain)); + ArgumentNullException.ThrowIfNull(auth0Settings.Audience, nameof(Auth0Settings.Audience)); + + builder.Services.Configure(section); + + builder.AddJwtBearer(auth0Settings.SchemeName, options => + { + options.Authority = auth0Settings.Domain; + options.Audience = auth0Settings.Audience; + }); + + builder.Services.TryAddSingleton(); + builder.Services.AddHttpClient(); + } } /// diff --git a/src/SimpleAuthentication/Swagger/AuthenticationOperationFilter.cs b/src/SimpleAuthentication/Swagger/AuthenticationOperationFilter.cs index bfe0961..4508834 100644 --- a/src/SimpleAuthentication/Swagger/AuthenticationOperationFilter.cs +++ b/src/SimpleAuthentication/Swagger/AuthenticationOperationFilter.cs @@ -8,6 +8,7 @@ using Microsoft.Extensions.Options; using Microsoft.OpenApi.Models; using SimpleAuthentication.ApiKey; +using SimpleAuthentication.Auth0; using SimpleAuthentication.JwtBearer; using Swashbuckle.AspNetCore.SwaggerGen; @@ -18,12 +19,14 @@ internal class AuthenticationOperationFilter : IOperationFilter private readonly IAuthorizationPolicyProvider authorizationPolicyProvider; private readonly JwtBearerSettings jwtBearerSettings; private readonly ApiKeySettings apiKeySettings; + private readonly Auth0Settings auth0Settings; - public AuthenticationOperationFilter(IAuthorizationPolicyProvider authorizationPolicyProvider, IOptions jwtBearerSettingsOptions, IOptions apiKeySettingsOptions) + public AuthenticationOperationFilter(IAuthorizationPolicyProvider authorizationPolicyProvider, IOptions jwtBearerSettingsOptions, IOptions apiKeySettingsOptions, IOptions auth0SettingsOptions) { this.authorizationPolicyProvider = authorizationPolicyProvider; jwtBearerSettings = jwtBearerSettingsOptions.Value; apiKeySettings = apiKeySettingsOptions.Value; + auth0Settings = auth0SettingsOptions.Value; } public void Apply(OpenApiOperation operation, OperationFilterContext context) @@ -49,6 +52,9 @@ public void Apply(OpenApiOperation operation, OperationFilterContext context) CheckAddSecurityRequirement(operation, hasApiKeyHeaderAuthentication ? $"{apiKeySettings.SchemeName} in Header" : null); CheckAddSecurityRequirement(operation, hasApiKeyQueryAuthentication ? $"{apiKeySettings.SchemeName} in Query String" : null); + var hasAuth0Authentication = !string.IsNullOrWhiteSpace(auth0Settings.Domain); + CheckAddSecurityRequirement(operation, hasAuth0Authentication ? $"{auth0Settings.SchemeName} Bearer" : null); + operation.Responses.TryAdd(StatusCodes.Status401Unauthorized.ToString(), GetResponse(HttpStatusCode.Unauthorized.ToString())); operation.Responses.TryAdd(StatusCodes.Status403Forbidden.ToString(), GetResponse(HttpStatusCode.Forbidden.ToString())); }