Skip to content

Commit

Permalink
完善 OIDC 支持
Browse files Browse the repository at this point in the history
  • Loading branch information
agile.zhou committed Aug 20, 2023
1 parent acb13bc commit 6b06158
Show file tree
Hide file tree
Showing 16 changed files with 167 additions and 63 deletions.
2 changes: 2 additions & 0 deletions AgileConfig.Server.Apisite/Controllers/AdminController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -131,6 +132,7 @@ public async Task<IActionResult> OidcLoginByCode(string code)
Source = UserSource.SSO
};
await _userService.AddAsync(newUser);
await _userService.UpdateUserRolesAsync(newUser.Id, new List<Role> { Role.NormalUser });
}

var response = await LoginSuccessful(userInfo.UserName);
Expand Down
10 changes: 5 additions & 5 deletions AgileConfig.Server.Apisite/Controllers/UserController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -111,10 +111,10 @@ public async Task<IActionResult> 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();
Expand All @@ -124,8 +124,8 @@ public async Task<IActionResult> Add([FromBody] UserVM model)

return Json(new
{
success = result && reuslt1,
message = !(result && reuslt1) ? "添加用户失败,请查看错误日志" : ""
success = addUserResult && addUserRoleResult,
message = !(addUserResult && addUserRoleResult) ? "添加用户失败,请查看错误日志" : ""
});
}

Expand Down
22 changes: 12 additions & 10 deletions AgileConfig.Server.Apisite/appsettings.Development.json
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
}
22 changes: 12 additions & 10 deletions AgileConfig.Server.Apisite/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
}
4 changes: 2 additions & 2 deletions AgileConfig.Server.Data.Entity/UserRole.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@ public enum Role
[Description("超级管理员")]
SuperAdmin = 0,
[Description("管理员")]
Admin,
Admin = 1,
[Description("操作员")]
NormalUser,
NormalUser = 2,
}

public enum AppRole
Expand Down
2 changes: 1 addition & 1 deletion AgileConfig.Server.OIDC/IOidcClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@ public interface IOidcClient
OidcSetting OidcSetting { get; }
string GetAuthorizeUrl();
(string Id, string UserName) UnboxIdToken(string idToken);
Task<TokenModel> Validate(string code);
Task<(string IdToken, string accessToken)> Validate(string code);
}
}
48 changes: 23 additions & 25 deletions AgileConfig.Server.OIDC/OidcClient.cs
Original file line number Diff line number Diff line change
@@ -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
{
Expand Down Expand Up @@ -27,39 +30,34 @@ public string GetAuthorizeUrl()
return url;
}

public async Task<TokenModel> Validate(string code)
public async Task<(string IdToken, string accessToken)> Validate(string code)
{
var httpclient = new HttpClient();
var kvs = new List<KeyValuePair<string, string>>() {
new KeyValuePair<string, string>("code", code),
new KeyValuePair<string, string>("grant_type", "authorization_code"),
new KeyValuePair<string, string>("redirect_uri", _oidcSetting.RedirectUri),
new KeyValuePair<string, string>("client_id", _oidcSetting.ClientId),
new KeyValuePair<string, string>("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<dynamic>(bodyJson);
string access_token = responseObject.access_token;
var responseObject = JsonConvert.DeserializeObject<TokenEndpointResponseModel>(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)
Expand All @@ -72,10 +70,10 @@ public async Task<TokenModel> 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; }
}
}
6 changes: 5 additions & 1 deletion AgileConfig.Server.OIDC/OidcSetting.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ public class OidcSetting
string authorizationEndpoint,
string userIdClaim,
string userNameClaim,
string scope)
string scope,
string tokenEndpointAuthMethod
)
{
ClientId = clientId;
ClientSecret = clientSecret;
Expand All @@ -20,6 +22,7 @@ public class OidcSetting
UserIdClaim = userIdClaim;
UserNameClaim = userNameClaim;
Scope = scope;
TokenEndpointAuthMethod = tokenEndpointAuthMethod;
}

public string ClientId { get; }
Expand All @@ -30,5 +33,6 @@ public class OidcSetting
public string UserIdClaim { get; }
public string UserNameClaim { get; }
public string Scope { get; }
public string TokenEndpointAuthMethod { get; set; }
}
}
3 changes: 2 additions & 1 deletion AgileConfig.Server.OIDC/ServiceCollectionExt.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Microsoft.Extensions.DependencyInjection;
using AgileConfig.Server.OIDC.SettingProvider;
using Microsoft.Extensions.DependencyInjection;

namespace AgileConfig.Server.OIDC
{
Expand Down
Original file line number Diff line number Diff line change
@@ -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
{
Expand All @@ -17,14 +17,15 @@ public ConfigfileOidcSettingProvider(ILogger<ConfigfileOidcSettingProvider> 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} " +
Expand All @@ -34,7 +35,8 @@ public ConfigfileOidcSettingProvider(ILogger<ConfigfileOidcSettingProvider> logg
$"userIdClaim:{userIdClaim} " +
$"userNameClaim:{userNameClaim} " +
$"scope:{scope} " +
$"loginButtonText:{loginButtonText} "
$"loginButtonText:{loginButtonText} " +
$"tokenEndpointAuthMethod:{tokenEndpointAuthMethod} "
);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
namespace AgileConfig.Server.OIDC
namespace AgileConfig.Server.OIDC.SettingProvider
{
public interface IOidcSettingProvider
{
Expand Down
Original file line number Diff line number Diff line change
@@ -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<KeyValuePair<string, string>>() {
new KeyValuePair<string, string>("code", code),
new KeyValuePair<string, string>("grant_type", "authorization_code"),
new KeyValuePair<string, string>("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);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace AgileConfig.Server.OIDC.TokenEndpointAuthMethods
{
internal interface ITokenEndpointAuthMethod
{
(HttpContent HttpContent, string BasicAuthorizationString) GetAuthHttpContent(string code, OidcSetting oidcSetting);
}
}
Original file line number Diff line number Diff line change
@@ -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<KeyValuePair<string, string>>() {
new KeyValuePair<string, string>("code", code),
new KeyValuePair<string, string>("grant_type", "authorization_code"),
new KeyValuePair<string, string>("redirect_uri", oidcSetting.RedirectUri)
};
var httpContent = new FormUrlEncodedContent(kvs);

return (httpContent, "");
}
}
}
Original file line number Diff line number Diff line change
@@ -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<KeyValuePair<string, string>>() {
new KeyValuePair<string, string>("code", code),
new KeyValuePair<string, string>("grant_type", "authorization_code"),
new KeyValuePair<string, string>("redirect_uri", oidcSetting.RedirectUri),
new KeyValuePair<string, string>("client_id", oidcSetting.ClientId),
new KeyValuePair<string, string>("client_secret", oidcSetting.ClientSecret),
};
var httpContent = new FormUrlEncodedContent(kvs);

return (httpContent, "");
}
}
}
Original file line number Diff line number Diff line change
@@ -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();
}
}
}
}

0 comments on commit 6b06158

Please sign in to comment.