diff --git a/AgileConfig.Server.Apisite/Controllers/AdminController.cs b/AgileConfig.Server.Apisite/Controllers/AdminController.cs index 7d3b1f32..39b770fd 100644 --- a/AgileConfig.Server.Apisite/Controllers/AdminController.cs +++ b/AgileConfig.Server.Apisite/Controllers/AdminController.cs @@ -11,6 +11,7 @@ using System.Dynamic; using AgileConfig.Server.Apisite.Utilites; using AgileConfig.Server.OIDC; +using System.Collections.Generic; namespace AgileConfig.Server.Apisite.Controllers { @@ -131,6 +132,7 @@ public async Task OidcLoginByCode(string code) Source = UserSource.SSO }; await _userService.AddAsync(newUser); + await _userService.UpdateUserRolesAsync(newUser.Id, new List { Role.NormalUser }); } var response = await LoginSuccessful(userInfo.UserName); diff --git a/AgileConfig.Server.Apisite/Controllers/UserController.cs b/AgileConfig.Server.Apisite/Controllers/UserController.cs index 7c991659..39a5129a 100644 --- a/AgileConfig.Server.Apisite/Controllers/UserController.cs +++ b/AgileConfig.Server.Apisite/Controllers/UserController.cs @@ -111,10 +111,10 @@ public async Task Add([FromBody] UserVM model) user.CreateTime = DateTime.Now; user.UserName = model.UserName; - var result = await _userService.AddAsync(user); - var reuslt1 = await _userService.UpdateUserRolesAsync(user.Id, model.UserRoles); + var addUserResult = await _userService.AddAsync(user); + var addUserRoleResult = await _userService.UpdateUserRolesAsync(user.Id, model.UserRoles); - if (result) + if (addUserResult) { dynamic param = new ExpandoObject(); param.userName = this.GetCurrentUserName(); @@ -124,8 +124,8 @@ public async Task Add([FromBody] UserVM model) return Json(new { - success = result && reuslt1, - message = !(result && reuslt1) ? "添加用户失败,请查看错误日志" : "" + success = addUserResult && addUserRoleResult, + message = !(addUserResult && addUserRoleResult) ? "添加用户失败,请查看错误日志" : "" }); } diff --git a/AgileConfig.Server.Apisite/appsettings.Development.json b/AgileConfig.Server.Apisite/appsettings.Development.json index a62e05af..e6613074 100644 --- a/AgileConfig.Server.Apisite/appsettings.Development.json +++ b/AgileConfig.Server.Apisite/appsettings.Development.json @@ -40,18 +40,20 @@ "Audience": "agileconfig.admin", // 接收者 "ExpireSeconds": 86400 // 过期时间 }, + "SSO": { - "enabled": false, - "loginButtonText": "SSO", + "enabled": true, // 是否启用 SSO + "loginButtonText": "SSO",// 自定义 SSO 跳转按钮的文字 "OIDC": { - "clientId": "2bb823b7-f1ad-48c7-a9a1-713e9a885a5d", - "clientSecret": "", - "redirectUri": "http://localhost:5000/sso", - "tokenEndpoint": "https://login.microsoftonline.com/7aa25791-9a8c-4be4-872f-289bfec8cddb/oauth2/v2.0/token", - "authorizationEndpoint": "https://login.microsoftonline.com/7aa25791-9a8c-4be4-872f-289bfec8cddb/oauth2/v2.0/authorize", - "userIdClaim": "sub", - "userNameClaim": "name", - "scope": "openid profile" + "clientId": "2bb823b7-f1ad-48c7-a9a1-713e9a885a5d", // 应用程序ID + "clientSecret": "", // 应用程序密钥 + "redirectUri": "http://localhost:5000/sso", //OIDC Server 授权成功后的回调地址 + "tokenEndpoint": "https://login.microsoftonline.com/7aa25791-9a8c-4be4-872f-289bfec8cddb/oauth2/v2.0/token", // Token Endpoint, code换取token的地址 + "tokenEndpointAuthMethod": "client_secret_post", //获取token的接口的认证方案:client_secret_post, client_secret_basic, none. default=client_secret_post. + "authorizationEndpoint": "https://login.microsoftonline.com/7aa25791-9a8c-4be4-872f-289bfec8cddb/oauth2/v2.0/authorize", // OIDC Server 授权地址 + "userIdClaim": "sub", // id token 中用户ID的 Claim key + "userNameClaim": "name", // id token 用户名的Claim key + "scope": "openid profile" // 请求的scope } } } diff --git a/AgileConfig.Server.Apisite/appsettings.json b/AgileConfig.Server.Apisite/appsettings.json index 43878fa2..f245b60f 100644 --- a/AgileConfig.Server.Apisite/appsettings.json +++ b/AgileConfig.Server.Apisite/appsettings.json @@ -41,18 +41,20 @@ "Audience": "agileconfig.admin", // 接收者 "ExpireSeconds": 86400 // 过期时间 }, + "SSO": { - "enabled": false, - "loginButtonText": "", + "enabled": false, // 是否启用 SSO + "loginButtonText": "", // 自定义 SSO 跳转按钮的文字 "OIDC": { - "clientId": "", - "clientSecret": "", - "redirectUri": "", - "tokenEndpoint": "", - "authorizationEndpoint": "", - "userIdClaim": "sub", - "userNameClaim": "name", - "scope": "openid profile" + "clientId": "", // 应用程序ID + "clientSecret": "", // 应用程序密钥 + "redirectUri": "", //OIDC Server 授权成功后的回调地址 + "tokenEndpoint": "", // Token Endpoint, code换取token的地址 + "tokenEndpointAuthMethod": "client_secret_post", //获取token的接口的认证方案:client_secret_post, client_secret_basic, none. default=client_secret_post. + "authorizationEndpoint": "", // OIDC Server 授权地址 + "userIdClaim": "sub", // id token 中用户ID的 Claim key + "userNameClaim": "name", // id token 用户名的Claim key + "scope": "openid profile" // 请求的scope } } } diff --git a/AgileConfig.Server.Data.Entity/UserRole.cs b/AgileConfig.Server.Data.Entity/UserRole.cs index 3eccd429..04e902a1 100644 --- a/AgileConfig.Server.Data.Entity/UserRole.cs +++ b/AgileConfig.Server.Data.Entity/UserRole.cs @@ -27,9 +27,9 @@ public enum Role [Description("超级管理员")] SuperAdmin = 0, [Description("管理员")] - Admin, + Admin = 1, [Description("操作员")] - NormalUser, + NormalUser = 2, } public enum AppRole diff --git a/AgileConfig.Server.OIDC/IOidcClient.cs b/AgileConfig.Server.OIDC/IOidcClient.cs index 29ecdbf2..2afbbc29 100644 --- a/AgileConfig.Server.OIDC/IOidcClient.cs +++ b/AgileConfig.Server.OIDC/IOidcClient.cs @@ -5,6 +5,6 @@ public interface IOidcClient OidcSetting OidcSetting { get; } string GetAuthorizeUrl(); (string Id, string UserName) UnboxIdToken(string idToken); - Task Validate(string code); + Task<(string IdToken, string accessToken)> Validate(string code); } } \ No newline at end of file diff --git a/AgileConfig.Server.OIDC/OidcClient.cs b/AgileConfig.Server.OIDC/OidcClient.cs index 22b8c866..b13fcab8 100644 --- a/AgileConfig.Server.OIDC/OidcClient.cs +++ b/AgileConfig.Server.OIDC/OidcClient.cs @@ -1,5 +1,8 @@ -using Newtonsoft.Json; +using AgileConfig.Server.OIDC.SettingProvider; +using AgileConfig.Server.OIDC.TokenEndpointAuthMethods; +using Newtonsoft.Json; using System.IdentityModel.Tokens.Jwt; +using System.Net.Http.Headers; namespace AgileConfig.Server.OIDC { @@ -27,39 +30,34 @@ public string GetAuthorizeUrl() return url; } - public async Task Validate(string code) + public async Task<(string IdToken, string accessToken)> Validate(string code) { - var httpclient = new HttpClient(); - var kvs = new List>() { - new KeyValuePair("code", code), - new KeyValuePair("grant_type", "authorization_code"), - new KeyValuePair("redirect_uri", _oidcSetting.RedirectUri), - new KeyValuePair("client_id", _oidcSetting.ClientId), - new KeyValuePair("client_secret", _oidcSetting.ClientSecret), - }; - var form = new FormUrlEncodedContent(kvs); - var response = await httpclient.PostAsync(_oidcSetting.TokenEndpoint, form); + var authMethod = TokenEndpointAuthMethodFactory.Create(_oidcSetting.TokenEndpointAuthMethod); + var httpContent = authMethod.GetAuthHttpContent(code, _oidcSetting); + + using var httpclient = new HttpClient(); + if (!string.IsNullOrEmpty(httpContent.BasicAuthorizationString)) + { + httpclient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", httpContent.BasicAuthorizationString); + } + var response = await httpclient.PostAsync(_oidcSetting.TokenEndpoint, httpContent.HttpContent); response.EnsureSuccessStatusCode(); + var bodyJson = await response.Content.ReadAsStringAsync(); if (string.IsNullOrWhiteSpace(bodyJson)) { - throw new Exception("Can not validate the code. The token endpoint return the empty response."); + throw new Exception("Can not validate the code. Token endpoint return empty response."); } - dynamic responseObject = JsonConvert.DeserializeObject(bodyJson); - string access_token = responseObject.access_token; + var responseObject = JsonConvert.DeserializeObject(bodyJson); string id_token = responseObject.id_token; - if (string.IsNullOrWhiteSpace(access_token) || string.IsNullOrWhiteSpace(id_token)) + if (string.IsNullOrWhiteSpace(id_token)) { - throw new Exception("Can not validate the code. Access token or Id token missing."); + throw new Exception("Can not validate the code. Id token missing."); } - var obj = new TokenModel(); - obj.IdToken = id_token; - obj.AccessToken = access_token; - - return obj; + return (id_token, ""); } public (string Id, string UserName) UnboxIdToken(string idToken) @@ -72,10 +70,10 @@ public async Task Validate(string code) } } - public class TokenModel + internal class TokenEndpointResponseModel { - public string IdToken { get; set;} + public string id_token { get; set; } - public string AccessToken { get; set;} + public string access_token { get; set; } } } \ No newline at end of file diff --git a/AgileConfig.Server.OIDC/OidcSetting.cs b/AgileConfig.Server.OIDC/OidcSetting.cs index 5f5470f6..f9ee165b 100644 --- a/AgileConfig.Server.OIDC/OidcSetting.cs +++ b/AgileConfig.Server.OIDC/OidcSetting.cs @@ -10,7 +10,9 @@ public OidcSetting( string authorizationEndpoint, string userIdClaim, string userNameClaim, - string scope) + string scope, + string tokenEndpointAuthMethod + ) { ClientId = clientId; ClientSecret = clientSecret; @@ -20,6 +22,7 @@ public OidcSetting( UserIdClaim = userIdClaim; UserNameClaim = userNameClaim; Scope = scope; + TokenEndpointAuthMethod = tokenEndpointAuthMethod; } public string ClientId { get; } @@ -30,5 +33,6 @@ public OidcSetting( public string UserIdClaim { get; } public string UserNameClaim { get; } public string Scope { get; } + public string TokenEndpointAuthMethod { get; set; } } } diff --git a/AgileConfig.Server.OIDC/ServiceCollectionExt.cs b/AgileConfig.Server.OIDC/ServiceCollectionExt.cs index bd28096e..d38fd6a4 100644 --- a/AgileConfig.Server.OIDC/ServiceCollectionExt.cs +++ b/AgileConfig.Server.OIDC/ServiceCollectionExt.cs @@ -1,4 +1,5 @@ -using Microsoft.Extensions.DependencyInjection; +using AgileConfig.Server.OIDC.SettingProvider; +using Microsoft.Extensions.DependencyInjection; namespace AgileConfig.Server.OIDC { diff --git a/AgileConfig.Server.OIDC/ConfigfileOidcSettingProvider.cs b/AgileConfig.Server.OIDC/SettingProvider/ConfigfileOidcSettingProvider.cs similarity index 75% rename from AgileConfig.Server.OIDC/ConfigfileOidcSettingProvider.cs rename to AgileConfig.Server.OIDC/SettingProvider/ConfigfileOidcSettingProvider.cs index 81ff5709..1ce07df2 100644 --- a/AgileConfig.Server.OIDC/ConfigfileOidcSettingProvider.cs +++ b/AgileConfig.Server.OIDC/SettingProvider/ConfigfileOidcSettingProvider.cs @@ -1,7 +1,7 @@ using AgileConfig.Server.Common; using Microsoft.Extensions.Logging; -namespace AgileConfig.Server.OIDC +namespace AgileConfig.Server.OIDC.SettingProvider { public class ConfigfileOidcSettingProvider : IOidcSettingProvider { @@ -17,14 +17,15 @@ public ConfigfileOidcSettingProvider(ILogger logg var userIdClaim = Global.Config["SSO:OIDC:userIdClaim"]; var userNameClaim = Global.Config["SSO:OIDC:userNameClaim"]; var scope = Global.Config["SSO:OIDC:scope"]; + var tokenEndpointAuthMethod = Global.Config["SSO:OIDC:tokenEndpointAuthMethod"]; var loginButtonText = Global.Config["SSO:loginButtonText"]; _oidcSetting = new OidcSetting( - clientId, - clientSecret, redirectUri, - tokenEndpoint, authorizationEndpoint, - userIdClaim, userNameClaim, - scope); + clientId, + clientSecret, redirectUri, + tokenEndpoint, authorizationEndpoint, + userIdClaim, userNameClaim, + scope, tokenEndpointAuthMethod); logger.LogInformation($"OIDC Setting " + $"clientId:{clientId} " + @@ -34,7 +35,8 @@ public ConfigfileOidcSettingProvider(ILogger logg $"userIdClaim:{userIdClaim} " + $"userNameClaim:{userNameClaim} " + $"scope:{scope} " + - $"loginButtonText:{loginButtonText} " + $"loginButtonText:{loginButtonText} " + + $"tokenEndpointAuthMethod:{tokenEndpointAuthMethod} " ); } diff --git a/AgileConfig.Server.OIDC/IOidcSettingProvider.cs b/AgileConfig.Server.OIDC/SettingProvider/IOidcSettingProvider.cs similarity index 63% rename from AgileConfig.Server.OIDC/IOidcSettingProvider.cs rename to AgileConfig.Server.OIDC/SettingProvider/IOidcSettingProvider.cs index 1cf460a4..2b0dc230 100644 --- a/AgileConfig.Server.OIDC/IOidcSettingProvider.cs +++ b/AgileConfig.Server.OIDC/SettingProvider/IOidcSettingProvider.cs @@ -1,4 +1,4 @@ -namespace AgileConfig.Server.OIDC +namespace AgileConfig.Server.OIDC.SettingProvider { public interface IOidcSettingProvider { diff --git a/AgileConfig.Server.OIDC/TokenEndpointAuthMethods/BasicTokenEndpointAuthMethod.cs b/AgileConfig.Server.OIDC/TokenEndpointAuthMethods/BasicTokenEndpointAuthMethod.cs new file mode 100644 index 00000000..6b01302b --- /dev/null +++ b/AgileConfig.Server.OIDC/TokenEndpointAuthMethods/BasicTokenEndpointAuthMethod.cs @@ -0,0 +1,23 @@ +using System.Net.Http.Headers; +using System.Text; + +namespace AgileConfig.Server.OIDC.TokenEndpointAuthMethods +{ + internal class BasicTokenEndpointAuthMethod : ITokenEndpointAuthMethod + { + public (HttpContent HttpContent, string BasicAuthorizationString) GetAuthHttpContent(string code, OidcSetting oidcSetting) + { + var kvs = new List>() { + new KeyValuePair("code", code), + new KeyValuePair("grant_type", "authorization_code"), + new KeyValuePair("redirect_uri", oidcSetting.RedirectUri) + }; + var httpContent = new FormUrlEncodedContent(kvs); + + var txt = $"{oidcSetting.ClientId}:{oidcSetting.ClientSecret}"; + string authorizationString = Convert.ToBase64String(Encoding.UTF8.GetBytes(txt)); + + return (httpContent, authorizationString); + } + } +} diff --git a/AgileConfig.Server.OIDC/TokenEndpointAuthMethods/ITokenEndpointAuthMethod.cs b/AgileConfig.Server.OIDC/TokenEndpointAuthMethods/ITokenEndpointAuthMethod.cs new file mode 100644 index 00000000..35e604ac --- /dev/null +++ b/AgileConfig.Server.OIDC/TokenEndpointAuthMethods/ITokenEndpointAuthMethod.cs @@ -0,0 +1,7 @@ +namespace AgileConfig.Server.OIDC.TokenEndpointAuthMethods +{ + internal interface ITokenEndpointAuthMethod + { + (HttpContent HttpContent, string BasicAuthorizationString) GetAuthHttpContent(string code, OidcSetting oidcSetting); + } +} diff --git a/AgileConfig.Server.OIDC/TokenEndpointAuthMethods/NoneTokenEndpointAuthMethod.cs b/AgileConfig.Server.OIDC/TokenEndpointAuthMethods/NoneTokenEndpointAuthMethod.cs new file mode 100644 index 00000000..695e8e11 --- /dev/null +++ b/AgileConfig.Server.OIDC/TokenEndpointAuthMethods/NoneTokenEndpointAuthMethod.cs @@ -0,0 +1,19 @@ +using System.Text; + +namespace AgileConfig.Server.OIDC.TokenEndpointAuthMethods +{ + internal class NoneTokenEndpointAuthMethod : ITokenEndpointAuthMethod + { + public (HttpContent HttpContent, string BasicAuthorizationString) GetAuthHttpContent(string code, OidcSetting oidcSetting) + { + var kvs = new List>() { + new KeyValuePair("code", code), + new KeyValuePair("grant_type", "authorization_code"), + new KeyValuePair("redirect_uri", oidcSetting.RedirectUri) + }; + var httpContent = new FormUrlEncodedContent(kvs); + + return (httpContent, ""); + } + } +} diff --git a/AgileConfig.Server.OIDC/TokenEndpointAuthMethods/PostTokenEndpointAuthMethod.cs b/AgileConfig.Server.OIDC/TokenEndpointAuthMethods/PostTokenEndpointAuthMethod.cs new file mode 100644 index 00000000..432d12fb --- /dev/null +++ b/AgileConfig.Server.OIDC/TokenEndpointAuthMethods/PostTokenEndpointAuthMethod.cs @@ -0,0 +1,19 @@ +namespace AgileConfig.Server.OIDC.TokenEndpointAuthMethods +{ + internal class PostTokenEndpointAuthMethod : ITokenEndpointAuthMethod + { + public (HttpContent HttpContent, string BasicAuthorizationString) GetAuthHttpContent(string code, OidcSetting oidcSetting) + { + var kvs = new List>() { + new KeyValuePair("code", code), + new KeyValuePair("grant_type", "authorization_code"), + new KeyValuePair("redirect_uri", oidcSetting.RedirectUri), + new KeyValuePair("client_id", oidcSetting.ClientId), + new KeyValuePair("client_secret", oidcSetting.ClientSecret), + }; + var httpContent = new FormUrlEncodedContent(kvs); + + return (httpContent, ""); + } + } +} diff --git a/AgileConfig.Server.OIDC/TokenEndpointAuthMethods/TokenEndpointAuthMethodFactory.cs b/AgileConfig.Server.OIDC/TokenEndpointAuthMethods/TokenEndpointAuthMethodFactory.cs new file mode 100644 index 00000000..979400ac --- /dev/null +++ b/AgileConfig.Server.OIDC/TokenEndpointAuthMethods/TokenEndpointAuthMethodFactory.cs @@ -0,0 +1,25 @@ +namespace AgileConfig.Server.OIDC.TokenEndpointAuthMethods +{ + internal static class TokenEndpointAuthMethodFactory + { + public static ITokenEndpointAuthMethod Create(string methodName) + { + if (methodName == "client_secret_basic") + { + return new BasicTokenEndpointAuthMethod(); + } + else if (methodName == "client_secret_post") + { + return new PostTokenEndpointAuthMethod(); + } + else if (methodName == "none") + { + return new NoneTokenEndpointAuthMethod(); + } + else + { + throw new NotImplementedException(); + } + } + } +}