From 3312f951d1704d24b08279b7d0c04cd7405db08b Mon Sep 17 00:00:00 2001 From: Mohammed Ahmed Hussien Date: Sat, 2 Mar 2024 03:36:57 +0300 Subject: [PATCH] Complete User Info End Poing --- Sample/ClientApp_OpenId/Program.cs | 2 +- Server/src/OAuth20.Server/Constants.cs | 2 +- .../DiscoveryEndpointController.cs | 2 +- .../Controllers/UserInfoController.cs | 10 ++++-- .../OauthResponse/DiscoveryResponse.cs | 2 +- Server/src/OAuth20.Server/Program.cs | 19 +++++++++--- .../Services/AuthorizeResultService.cs | 31 +++++++++++-------- .../OAuth20.Server/Services/IClientService.cs | 14 +++++++++ .../Services/UserInfoService.cs | 12 ++++--- .../appsettings.Development.json | 2 +- Server/src/OAuth20.Server/appsettings.json | 2 +- 11 files changed, 69 insertions(+), 29 deletions(-) diff --git a/Sample/ClientApp_OpenId/Program.cs b/Sample/ClientApp_OpenId/Program.cs index 72be4fc..1dede4c 100644 --- a/Sample/ClientApp_OpenId/Program.cs +++ b/Sample/ClientApp_OpenId/Program.cs @@ -23,7 +23,7 @@ options.CallbackPath = "/signin-oidc"; options.SaveTokens = true; options.Scope.Add("jwtapitestapp.read"); - // options.GetClaimsFromUserInfoEndpoint = true; + options.GetClaimsFromUserInfoEndpoint = true; }); diff --git a/Server/src/OAuth20.Server/Constants.cs b/Server/src/OAuth20.Server/Constants.cs index 4f1aa47..d5a80ef 100644 --- a/Server/src/OAuth20.Server/Constants.cs +++ b/Server/src/OAuth20.Server/Constants.cs @@ -45,7 +45,7 @@ public static class ContentTypeSupported public static class AuthenticatedRequestScheme { - public const string AuthorizationRequestHeader = "Beraer"; + public const string AuthorizationRequestHeader = "Bearer"; public const string FormEncodedBodyParameter = "access_token"; public const string UriQueryParameter = "access_token"; } diff --git a/Server/src/OAuth20.Server/Controllers/DiscoveryEndpointController.cs b/Server/src/OAuth20.Server/Controllers/DiscoveryEndpointController.cs index 3119116..42e5548 100644 --- a/Server/src/OAuth20.Server/Controllers/DiscoveryEndpointController.cs +++ b/Server/src/OAuth20.Server/Controllers/DiscoveryEndpointController.cs @@ -28,7 +28,7 @@ public JsonResult GetConfiguration() acr_values_supported = new string[] {"urn:mace:incommon:iap:silver", "urn:mace:incommon:iap:bronze"}, response_types_supported = new string[] { "code", "code id_token", "id_token", "token id_token" }, subject_types_supported = new string[] { "public", "pairwise" }, - user_info_endpoint = "https://localhost:7275/UserInfo/GetUserInfo", + userinfo_endpoint = "https://localhost:7275/api/UserInfo/GetUserInfo", userinfo_encryption_enc_values_supported = new string[] { "A128CBC-HS256", "A128GCM" }, id_token_signing_alg_values_supported = new string[] { "RS256", "ES256", "HS256" , "SHA256" }, id_token_encryption_alg_values_supported = new string[] { "RSA1_5", "A128KW" }, diff --git a/Server/src/OAuth20.Server/Controllers/UserInfoController.cs b/Server/src/OAuth20.Server/Controllers/UserInfoController.cs index 1155da1..243dc53 100644 --- a/Server/src/OAuth20.Server/Controllers/UserInfoController.cs +++ b/Server/src/OAuth20.Server/Controllers/UserInfoController.cs @@ -1,11 +1,15 @@ -using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Cors; +using Microsoft.AspNetCore.Mvc; using OAuth20.Server.Services; using System.Threading.Tasks; namespace OAuth20.Server.Controllers { [Route("api/[controller]")] + [EnableCors("UserInfoPolicy")] [ApiController] + [AllowAnonymous] public class UserInfoController : ControllerBase { private readonly IUserInfoService _userInfoService; @@ -13,7 +17,9 @@ public UserInfoController(IUserInfoService userInfoService) { _userInfoService = userInfoService; } - [HttpGet, HttpPost] + + [HttpGet("GetUserInfo")] + public async Task GetUserInfo() { var userInfo = await _userInfoService.GetUserInfoAsync(); diff --git a/Server/src/OAuth20.Server/OauthResponse/DiscoveryResponse.cs b/Server/src/OAuth20.Server/OauthResponse/DiscoveryResponse.cs index 536c7d5..da63497 100644 --- a/Server/src/OAuth20.Server/OauthResponse/DiscoveryResponse.cs +++ b/Server/src/OAuth20.Server/OauthResponse/DiscoveryResponse.cs @@ -17,7 +17,7 @@ public class DiscoveryResponse public string token_endpoint { get; set; } public IList token_endpoint_auth_methods_supported { get; set; } public IList token_endpoint_auth_signing_alg_values_supported { get; set; } - public string user_info_endpoint { get; set; } + public string userinfo_endpoint { get; set; } public string check_session_iframe { get; set; } public string end_session_endpoint { get; set; } public string jwks_uri { get; set; } diff --git a/Server/src/OAuth20.Server/Program.cs b/Server/src/OAuth20.Server/Program.cs index db0ca30..0c3bdf1 100644 --- a/Server/src/OAuth20.Server/Program.cs +++ b/Server/src/OAuth20.Server/Program.cs @@ -66,16 +66,27 @@ Everyone is permitted to copy and distribute verbatim copies builder.Services.AddScoped(); builder.Services.AddHttpContextAccessor(); -builder.Services.Configure(options => +//builder.Services.Configure(options => +//{ +// options.LowercaseQueryStrings = true; +// options.LowercaseUrls = true; +//}); + +builder.Services.AddCors(options => { - options.LowercaseQueryStrings = true; - options.LowercaseUrls = true; -}); + options.AddPolicy("UserInfoPolicy", o => + { + o.AllowAnyOrigin(); + o.AllowAnyHeader(); + o.AllowAnyMethod(); + }); +}); builder.Services.AddControllersWithViews(); var app = builder.Build(); app.UseStaticFiles(); app.UseRouting(); +app.UseCors("UserInfoPolicy"); app.UseAuthentication(); app.UseAuthorization(); app.UseEndpoints(endpoints => diff --git a/Server/src/OAuth20.Server/Services/AuthorizeResultService.cs b/Server/src/OAuth20.Server/Services/AuthorizeResultService.cs index 74b1126..af8237c 100644 --- a/Server/src/OAuth20.Server/Services/AuthorizeResultService.cs +++ b/Server/src/OAuth20.Server/Services/AuthorizeResultService.cs @@ -83,8 +83,8 @@ public AuthorizeResponse AuthorizeRequest(IHttpContextAccessor httpContextAccess // check the return url is match the one that in the client store - bool redirectUriIsMatched = client.Client.RedirectUri.Equals(authorizationRequest.redirect_uri,StringComparison.OrdinalIgnoreCase); - if(!redirectUriIsMatched) + bool redirectUriIsMatched = client.Client.RedirectUri.Equals(authorizationRequest.redirect_uri, StringComparison.OrdinalIgnoreCase); + if (!redirectUriIsMatched) { response.Error = ErrorTypeEnum.InvalidRequest.GetEnumDescription(); response.ErrorDescription = "redirect uri is not matched the one in the client store"; @@ -164,7 +164,7 @@ public TokenResponse GenerateToken(TokenRequest tokenRequest) } IEnumerable scopes = checkClientResult.Client.AllowedScopes.Intersect(tokenRequest.scope); - var clientCredentialAccessTokenResult = generateJWTTokne(scopes, Constants.TokenTypes.JWTAcceseccToken, checkClientResult.Client); + var clientCredentialAccessTokenResult = generateJWTTokne(scopes, Constants.TokenTypes.JWTAcceseccToken, checkClientResult.Client, null); SaveJWTTokenInBackStore(checkClientResult.Client.ClientId, clientCredentialAccessTokenResult.AccessToken, clientCredentialAccessTokenResult.ExpirationDate); result.access_token = clientCredentialAccessTokenResult.AccessToken; @@ -200,6 +200,7 @@ public TokenResponse GenerateToken(TokenRequest tokenRequest) string id_token = string.Empty; + string userId = null; if (clientCodeChecker.IsOpenId) { if (!clientCodeChecker.Subject.Identity.IsAuthenticated) @@ -208,7 +209,7 @@ public TokenResponse GenerateToken(TokenRequest tokenRequest) var currentUserName = clientCodeChecker.Subject.Identity.Name; - var userId = clientCodeChecker.Subject.Claims.FirstOrDefault(x => x.Type == ClaimTypes.NameIdentifier)?.Value; + userId = clientCodeChecker.Subject.Claims.FirstOrDefault(x => x.Type == ClaimTypes.NameIdentifier)?.Value; if (string.IsNullOrEmpty(userId) || string.IsNullOrEmpty(currentUserName)) return new TokenResponse { Error = ErrorTypeEnum.InvalidGrant.GetEnumDescription() }; @@ -226,10 +227,10 @@ public TokenResponse GenerateToken(TokenRequest tokenRequest) var claims = new List() { - new Claim("sub", userId), - new Claim("given_name", currentUserName), + new Claim("sub", userId, ClaimValueTypes.String), + new Claim("given_name", currentUserName, ClaimValueTypes.String), new Claim("iat", iat.ToString(), ClaimValueTypes.Integer), // time stamp - new Claim("nonce", clientCodeChecker.Nonce) + new Claim("nonce", clientCodeChecker.Nonce, ClaimValueTypes.String) }; foreach (var amr in amrs) claims.Add(new Claim("amr", amr));// authentication @@ -239,7 +240,6 @@ public TokenResponse GenerateToken(TokenRequest tokenRequest) var token = new JwtSecurityToken(_options.IDPUri, checkClientResult.Client.ClientId, claims, expires: DateTime.UtcNow.AddMinutes(int.Parse("50")), signingCredentials: new SigningCredentials(rsaSecurityKey, SecurityAlgorithms.RsaSha256)); - id_token = handler.WriteToken(token); var idptoken = new OAuthTokenEntity @@ -263,7 +263,7 @@ public TokenResponse GenerateToken(TokenRequest tokenRequest) where !OAuth2ServerHelpers.OpenIdConnectScopes.Contains(m) select m; - var accessTokenResult = generateJWTTokne(scopesinJWtAccessToken, Constants.TokenTypes.JWTAcceseccToken, checkClientResult.Client); + var accessTokenResult = generateJWTTokne(scopesinJWtAccessToken, Constants.TokenTypes.JWTAcceseccToken, checkClientResult.Client, userId); SaveJWTTokenInBackStore(checkClientResult.Client.ClientId, accessTokenResult.AccessToken, accessTokenResult.ExpirationDate); // here remove the code from the Concurrent Dictionary @@ -314,20 +314,25 @@ private bool codeVerifierIsSendByTheClientThatReceivedTheCode(string codeVerifie } - - public TokenResult generateJWTTokne(IEnumerable scopes, string tokenType, Client client) + + public TokenResult generateJWTTokne(IEnumerable scopes, string tokenType, Client client, string sub) { var result = new TokenResult(); if (tokenType == Constants.TokenTypes.JWTAcceseccToken) { - var claims_at = new List + var claims = new List { new Claim("scope", string.Join(' ', scopes)) }; + if (!string.IsNullOrEmpty(sub)) + { + claims.Add(new Claim("sub", sub, ClaimValueTypes.String)); + } + RSACryptoServiceProvider provider1 = new RSACryptoServiceProvider(); string publicPrivateKey1 = File.ReadAllText("PublicPrivateKey.xml"); @@ -336,7 +341,7 @@ public TokenResult generateJWTTokne(IEnumerable scopes, string tokenType RsaSecurityKey rsaSecurityKey1 = new RsaSecurityKey(provider1); JwtSecurityTokenHandler handler1 = new JwtSecurityTokenHandler(); - var token1 = new JwtSecurityToken(_options.IDPUri, client.ClientUri, claims_at, notBefore: DateTime.UtcNow, + var token1 = new JwtSecurityToken(_options.IDPUri, client.ClientUri, claims, notBefore: DateTime.UtcNow, expires: DateTime.UtcNow.AddMinutes(int.Parse("50")), signingCredentials: new SigningCredentials(rsaSecurityKey1, SecurityAlgorithms.RsaSha256)); diff --git a/Server/src/OAuth20.Server/Services/IClientService.cs b/Server/src/OAuth20.Server/Services/IClientService.cs index 72b2b32..762307e 100644 --- a/Server/src/OAuth20.Server/Services/IClientService.cs +++ b/Server/src/OAuth20.Server/Services/IClientService.cs @@ -30,6 +30,8 @@ CheckClientResult VerifyClientById(string clientId, bool checkWithSecret = false AudienceValidator ValidateAudienceHandler(IEnumerable audiences, SecurityToken securityToken, TokenValidationParameters validationParameters, Client client, string token); + Task GetClientByUriAsync(string clientUrl); + Task GetClientByIdAsync(string clientId); } @@ -66,6 +68,18 @@ public Task GetClientByIdAsync(string clientId) //} } + + public Task GetClientByUriAsync(string clientUrl) + { + var c = _clientStore.Clients.Where(x => x.ClientUri == clientUrl).FirstOrDefault(); + var response = new CheckClientResult + { + Client = c, + IsSuccess = true + }; + return Task.FromResult(response); + } + public CheckClientResult VerifyClientById(string clientId, bool checkWithSecret = false, string clientSecret = null, string grantType = null) { diff --git a/Server/src/OAuth20.Server/Services/UserInfoService.cs b/Server/src/OAuth20.Server/Services/UserInfoService.cs index 069899e..028133d 100644 --- a/Server/src/OAuth20.Server/Services/UserInfoService.cs +++ b/Server/src/OAuth20.Server/Services/UserInfoService.cs @@ -18,6 +18,7 @@ Everyone is permitted to copy and distribute verbatim copies using System.IdentityModel.Tokens.Jwt; using System.IO; using System.Linq; +using System.Security.Claims; using System.Security.Cryptography; using System.Threading.Tasks; @@ -75,8 +76,10 @@ public async Task GetUserInfoAsync() JwtSecurityTokenHandler jwtSecurityTokenHandler = new JwtSecurityTokenHandler(); JwtSecurityToken jwtSecurityToken = jwtSecurityTokenHandler.ReadJwtToken(bearerTokenUsages.Token); - var clientId = jwtSecurityToken.Audiences.FirstOrDefault(); - var client = await _clientService.GetClientByIdAsync(clientId); + var aud = jwtSecurityToken.Audiences.FirstOrDefault(); + var client = await _clientService.GetClientByUriAsync(aud); + + // TODO: // check if client is null. @@ -100,8 +103,9 @@ public async Task GetUserInfoAsync() if (tokenValidationReslt.IsValid) { - string userId = tokenValidationReslt.ClaimsIdentity.FindFirst("sub")?.Value; - + var payload = jwtSecurityToken.Payload; + //string userId = tokenValidationReslt.ClaimsIdentity.FindFirst("sub")?.Value; + var userId = payload.Claims.FirstOrDefault(x => x.Type == "sub")?.Value; // TODO: // check userId is null diff --git a/Server/src/OAuth20.Server/appsettings.Development.json b/Server/src/OAuth20.Server/appsettings.Development.json index 3f0295b..6725799 100644 --- a/Server/src/OAuth20.Server/appsettings.Development.json +++ b/Server/src/OAuth20.Server/appsettings.Development.json @@ -9,7 +9,7 @@ "BaseDBConnection": "Data Source=DEV01\\SQLEXPRESS;Initial Catalog=OAuth20_Db;Trusted_Connection=True;MultipleActiveResultSets=true" }, "AllowedHosts": "*", - "OAuthOption": { + "OAuthOptions": { "Provider": "InMemory", "IsAvaliable": true, "IDPUri": "https://localhost:7275" diff --git a/Server/src/OAuth20.Server/appsettings.json b/Server/src/OAuth20.Server/appsettings.json index 3f0295b..6725799 100644 --- a/Server/src/OAuth20.Server/appsettings.json +++ b/Server/src/OAuth20.Server/appsettings.json @@ -9,7 +9,7 @@ "BaseDBConnection": "Data Source=DEV01\\SQLEXPRESS;Initial Catalog=OAuth20_Db;Trusted_Connection=True;MultipleActiveResultSets=true" }, "AllowedHosts": "*", - "OAuthOption": { + "OAuthOptions": { "Provider": "InMemory", "IsAvaliable": true, "IDPUri": "https://localhost:7275"