🔄refactor: 使用OpenIddict 重构Identity

This commit is contained in:
yubaolee
2026-06-25 23:42:43 +08:00
parent 7a34fa7c51
commit 5e6ca18770
42 changed files with 697 additions and 2074 deletions

25
CheckAndKillPort12796.bat Normal file
View File

@@ -0,0 +1,25 @@
@echo off
chcp 936
setlocal enabledelayedexpansion
echo <20><><EFBFBD>ڼ<EFBFBD><DABC><EFBFBD>12796<39>˿<EFBFBD>ռ<EFBFBD><D5BC><EFBFBD><EFBFBD><EFBFBD><EFBFBD>...
rem <20><><EFBFBD><EFBFBD>ռ<EFBFBD><D5BC>12796<39>˿ڵĽ<DAB5><C4BD><EFBFBD>
FOR /F "tokens=5" %%i IN ('netstat -ano ^| findstr :12796 ^| findstr LISTENING') DO (
SET pid=%%i
echo <20><><EFBFBD><EFBFBD>ռ<EFBFBD><D5BC>12796<39>˿ڵĽ<DAB5><C4BD>̣<EFBFBD>PID: %%i
echo <20><><EFBFBD>ڲ<EFBFBD>ѯ<EFBFBD><D1AF><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ϣ...
tasklist | findstr %%i
echo <20><><EFBFBD>ڽ<EFBFBD><DABD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> %%i...
taskkill /F /PID %%i
echo <20><>֤<EFBFBD>˿<EFBFBD><CBBF>Ƿ<EFBFBD><C7B7><EFBFBD><EFBFBD>ͷ<EFBFBD>...
netstat -ano | findstr :12796
goto :END
)
echo δ<><CEB4><EFBFBD><EFBFBD>ռ<EFBFBD><D5BC>12796<39>˿ڵĽ<DAB5><C4BD>̡<EFBFBD>
:END
echo <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ɡ<EFBFBD>
pause

View File

@@ -6,7 +6,7 @@
public struct FlowInstanceStatus
{
/// <summary>
/// 召回
/// 召回/草稿状态
/// </summary>
public const int Draft = -1;
/// <summary>

View File

@@ -1,102 +0,0 @@
// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
using System.Collections.Generic;
using System.Security.Claims;
using IdentityModel;
using IdentityServer4;
using IdentityServer4.Models;
namespace OpenAuth.IdentityServer
{
public static class Config
{
public static IEnumerable<IdentityResource> GetIdentityResources()
{
return new IdentityResource[]
{
new IdentityResources.OpenId(),
new IdentityResources.Profile(),
};
}
/// <summary>
/// API信息
/// </summary>
/// <returns></returns>
public static IEnumerable<ApiResource> GetApis()
{
return new[]
{
new ApiResource("openauthapi", "OpenAuth.WebApi")
{
UserClaims = { ClaimTypes.Name, JwtClaimTypes.Name }
}
};
}
/// <summary>
/// 客户端信息
/// </summary>
/// <returns></returns>
public static IEnumerable<Client> GetClients(bool isProduction)
{
var host = "http://localhost";
if (isProduction)
{
host = "http://demo.openauth.net.cn"; //切换为自己的服务器信息
}
return new[]
{
new Client
{
ClientId = "OpenAuth.WebApi",//客户端名称
ClientName = "开源版webapi认证",//客户端描述
AllowedGrantTypes = GrantTypes.Implicit,//Implicit 方式
AllowAccessTokensViaBrowser = true,//是否通过浏览器为此客户端传输访问令牌
RedirectUris =
{
$"{host}:52789/swagger/oauth2-redirect.html"
},
AllowedScopes = { "openauthapi" }
},
new Client
{
ClientId = "OpenAuth.Mvc",
ClientName = "开源版mvc认证",
AllowedGrantTypes = GrantTypes.Implicit,
// 登录成功回调处理地址,处理回调返回的数据
RedirectUris = { $"{host}:1802/signin-oidc" },
// where to redirect to after logout
PostLogoutRedirectUris = { $"{host}:1802/signout-callback-oidc" },
AllowedScopes = new List<string>
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
"openauthapi"
}
},
new Client
{
ClientId = "OpenAuth.Pro",//企业版名称
ClientName = "企业版js请求认证",//企业版描述
AllowedGrantTypes = GrantTypes.Code,
RequirePkce = true,
RequireClientSecret = false,
RedirectUris = { $"{host}:1803/#/oidc-callback" },
PostLogoutRedirectUris = { $"{host}:1803" },
AllowedCorsOrigins = { $"{host}:1803" },
AllowedScopes =
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile, //请求用户的姓名,昵称等
"openauthapi"
}
}
};
}
}
}

View File

@@ -0,0 +1,165 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using Infrastructure;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using OpenAuth.App;
using OpenIddict.Abstractions;
using OpenIddict.Server.AspNetCore;
using static OpenIddict.Abstractions.OpenIddictConstants;
namespace OpenAuth.IdentityServer.Controllers
{
/// <summary>
/// 处理 OpenID Connect 授权请求
/// </summary>
public class AuthorizationController : Controller
{
private readonly UserManagerApp _userManager;
public AuthorizationController(UserManagerApp userManager)
{
_userManager = userManager;
}
/// <summary>
/// 处理授权端点请求 /connect/authorize
/// </summary>
[HttpGet("~/connect/authorize")]
[HttpPost("~/connect/authorize")]
public async Task<IActionResult> Authorize()
{
var request = HttpContext.GetOpenIddictServerRequest()
?? throw new InvalidOperationException("无法获取 OpenID Connect 请求。");
// 尝试获取已登录用户
var result = await HttpContext.AuthenticateAsync(CookieAuthenticationDefaults.AuthenticationScheme);
if (!result.Succeeded)
{
// 未登录 → 重定向到登录页面
return Challenge(
authenticationSchemes: new[] { CookieAuthenticationDefaults.AuthenticationScheme },
properties: new AuthenticationProperties
{
RedirectUri = Request.PathBase + Request.Path + QueryString.Create(
Request.HasFormContentType ? Request.Form.ToList() : Request.Query.ToList())
});
}
// 已登录 → 从 Cookie 中获取用户信息
var userId = result.Principal.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (string.IsNullOrEmpty(userId))
{
return Challenge(
authenticationSchemes: new[] { CookieAuthenticationDefaults.AuthenticationScheme },
properties: new AuthenticationProperties
{
RedirectUri = Request.PathBase + Request.Path + QueryString.Create(
Request.HasFormContentType ? Request.Form.ToList() : Request.Query.ToList())
});
}
// 加载用户信息
var user = GetUser(userId);
if (user == null)
{
return Forbid(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
}
// 构建 Claims
var claims = new List<Claim>
{
new Claim(Claims.Subject, user.Id),
new Claim(Claims.Name, user.Name ?? user.Account),
new Claim(ClaimTypes.Name, user.Account),
};
var identity = new ClaimsIdentity(claims, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
var principal = new ClaimsPrincipal(identity);
// 设置请求的 scopes
principal.SetScopes(request.GetScopes());
// 设置 access_token 的 audience对应 WebApi 的 options.Audience
principal.SetResources("openauthapi");
// 设置 Claims 目标(决定哪些 Claims 出现在 access_token / id_token 中)
foreach (var claim in principal.Claims)
{
claim.SetDestinations(GetDestinations(claim, principal));
}
// 签发授权码
return SignIn(principal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
}
/// <summary>
/// 处理登出端点请求 /connect/logout
/// </summary>
[HttpGet("~/connect/logout")]
[HttpPost("~/connect/logout")]
public async Task<IActionResult> Logout()
{
// 清除本地 Cookie
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
// 通知 OpenIddict 登出完成
return SignOut(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
}
/// <summary>
/// 根据用户ID获取用户
/// </summary>
private OpenAuth.Repository.Domain.SysUser GetUser(string id)
{
if (id == Define.SYSTEM_USERNAME)
{
return new OpenAuth.Repository.Domain.SysUser
{
Account = Define.SYSTEM_USERNAME,
Id = Define.SYSTEM_USERNAME,
Name = Define.SYSTEM_USERNAME
};
}
return _userManager.Get(id);
}
/// <summary>
/// 确定 Claim 的目标access_token / id_token
/// </summary>
private static IEnumerable<string> GetDestinations(Claim claim, ClaimsPrincipal principal)
{
switch (claim.Type)
{
case Claims.Name:
yield return Destinations.AccessToken;
if (principal.HasScope(Scopes.Profile))
yield return Destinations.IdentityToken;
yield break;
case ClaimTypes.Name:
yield return Destinations.AccessToken;
if (principal.HasScope(Scopes.Profile))
yield return Destinations.IdentityToken;
yield break;
case Claims.Subject:
yield return Destinations.AccessToken;
yield return Destinations.IdentityToken;
yield break;
default:
yield return Destinations.AccessToken;
yield break;
}
}
}
}

View File

@@ -0,0 +1,135 @@
using System;
using System.Collections.Generic;
using System.Security.Claims;
using System.Threading.Tasks;
using Infrastructure;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Mvc;
using OpenAuth.App;
using OpenIddict.Abstractions;
using OpenIddict.Server.AspNetCore;
using static OpenIddict.Abstractions.OpenIddictConstants;
namespace OpenAuth.IdentityServer.Controllers
{
/// <summary>
/// 处理 OpenID Connect Token 请求
/// </summary>
public class TokenController : Controller
{
private readonly UserManagerApp _userManager;
public TokenController(UserManagerApp userManager)
{
_userManager = userManager;
}
/// <summary>
/// 处理令牌端点请求 /connect/token
/// </summary>
[HttpPost("~/connect/token")]
public async Task<IActionResult> Exchange()
{
var request = HttpContext.GetOpenIddictServerRequest()
?? throw new InvalidOperationException("无法获取 OpenID Connect 请求。");
if (request.IsAuthorizationCodeGrantType())
{
// 从授权码中还原 ClaimsPrincipal
var result = await HttpContext.AuthenticateAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
if (!result.Succeeded)
{
return Forbid(
authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
properties: new AuthenticationProperties(new Dictionary<string, string>
{
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "授权码无效或已过期。"
}));
}
var principal = result.Principal;
// 可选:补充最新的用户信息到 Claims
var userId = principal.GetClaim(Claims.Subject);
if (!string.IsNullOrEmpty(userId))
{
var user = GetUser(userId);
if (user == null || user.Status != 0)
{
return Forbid(
authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
properties: new AuthenticationProperties(new Dictionary<string, string>
{
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "用户不存在或已被禁用。"
}));
}
}
// 设置 Claims 目标
foreach (var claim in principal.Claims)
{
claim.SetDestinations(GetDestinations(claim, principal));
}
// 确保 access_token 包含正确的 audience
principal.SetResources("openauthapi");
return SignIn(principal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
}
throw new InvalidOperationException("不支持的授权类型。");
}
/// <summary>
/// 根据用户ID获取用户
/// </summary>
private OpenAuth.Repository.Domain.SysUser GetUser(string id)
{
if (id == Define.SYSTEM_USERNAME)
{
return new OpenAuth.Repository.Domain.SysUser
{
Account = Define.SYSTEM_USERNAME,
Id = Define.SYSTEM_USERNAME,
Name = Define.SYSTEM_USERNAME,
Status = 0
};
}
return _userManager.Get(id);
}
/// <summary>
/// 确定 Claim 的目标
/// </summary>
private static IEnumerable<string> GetDestinations(Claim claim, ClaimsPrincipal principal)
{
switch (claim.Type)
{
case Claims.Name:
yield return Destinations.AccessToken;
if (principal.HasScope(Scopes.Profile))
yield return Destinations.IdentityToken;
yield break;
case ClaimTypes.Name:
yield return Destinations.AccessToken;
if (principal.HasScope(Scopes.Profile))
yield return Destinations.IdentityToken;
yield break;
case Claims.Subject:
yield return Destinations.AccessToken;
yield return Destinations.IdentityToken;
yield break;
default:
yield return Destinations.AccessToken;
yield break;
}
}
}
}

View File

@@ -0,0 +1,92 @@
using System.Collections.Generic;
using System.Security.Claims;
using System.Threading.Tasks;
using Infrastructure;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Mvc;
using OpenAuth.App;
using OpenIddict.Abstractions;
using OpenIddict.Server.AspNetCore;
using static OpenIddict.Abstractions.OpenIddictConstants;
namespace OpenAuth.IdentityServer.Controllers
{
/// <summary>
/// 处理 OpenID Connect Userinfo 请求
/// </summary>
public class UserinfoController : Controller
{
private readonly UserManagerApp _userManager;
public UserinfoController(UserManagerApp userManager)
{
_userManager = userManager;
}
/// <summary>
/// 处理 /connect/userinfo 请求,返回已认证用户的个人信息
/// </summary>
[HttpGet("~/connect/userinfo")]
[HttpPost("~/connect/userinfo")]
public async Task<IActionResult> Userinfo()
{
var result = await HttpContext.AuthenticateAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
if (!result.Succeeded)
{
return Challenge(
authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
properties: new AuthenticationProperties(new Dictionary<string, string>
{
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidToken,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "访问令牌无效或已过期。"
}));
}
var userId = result.Principal.GetClaim(Claims.Subject);
if (string.IsNullOrEmpty(userId))
{
return Challenge(
authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
properties: new AuthenticationProperties(new Dictionary<string, string>
{
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidToken,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "令牌中缺少用户标识。"
}));
}
// 构建 userinfo 响应
var claims = new Dictionary<string, object>
{
[Claims.Subject] = userId
};
// 如果请求了 profile scope返回用户名称信息
if (result.Principal.HasScope(Scopes.Profile))
{
var user = GetUser(userId);
if (user != null)
{
claims[Claims.Name] = user.Name ?? user.Account;
claims[Claims.PreferredUsername] = user.Account;
}
}
return Ok(claims);
}
private OpenAuth.Repository.Domain.SysUser GetUser(string id)
{
if (id == Define.SYSTEM_USERNAME)
{
return new OpenAuth.Repository.Domain.SysUser
{
Account = Define.SYSTEM_USERNAME,
Id = Define.SYSTEM_USERNAME,
Name = Define.SYSTEM_USERNAME
};
}
return _userManager.Get(id);
}
}
}

View File

@@ -1,101 +0,0 @@
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using IdentityModel;
using IdentityServer4.Extensions;
using IdentityServer4.Models;
using IdentityServer4.Services;
using IdentityServer4.Test;
using Infrastructure;
using Microsoft.Extensions.Logging;
using OpenAuth.App;
using OpenAuth.Repository.Domain;
namespace OpenAuth.IdentityServer
{
public class CustomProfileService : IProfileService
{
/// <summary>
/// The logger
/// </summary>
protected readonly ILogger Logger;
protected UserManagerApp UserManager;
/// <summary>
/// Initializes a new instance of the <see cref="TestUserProfileService"/> class.
/// </summary>
/// <param name="users">The users.</param>
/// <param name="logger">The logger.</param>
public CustomProfileService( ILogger<TestUserProfileService> logger, UserManagerApp userManager)
{
Logger = logger;
UserManager = userManager;
}
/// <summary>
/// 只要有关用户的身份信息单元被请求(例如在令牌创建期间或通过用户信息终点),就会调用此方法
/// </summary>
/// <param name="context">The context.</param>
/// <returns></returns>
public virtual Task GetProfileDataAsync(ProfileDataRequestContext context)
{
context.LogProfileRequest(Logger);
//判断是否有请求Claim信息
if (context.RequestedClaimTypes.Any())
{
var user = GetUserById(context.Subject.GetSubjectId());
if (user != null)
{
//调用此方法以后内部会进行过滤只将用户请求的Claim加入到 context.IssuedClaims 集合中 这样我们的请求方便能正常获取到所需Claim
var claims = new[]
{
new Claim(ClaimTypes.Name, user.Account), //请求用户的账号这个可以保证User.Identity.Name有值
new Claim(JwtClaimTypes.Name, user.Name), //请求用户的姓名
};
//返回apiresource中定义的claims
context.AddRequestedClaims(claims);
}
}
context.LogIssuedClaims(Logger);
return Task.CompletedTask;
}
/// <summary>
/// 验证用户是否有效 例如token创建或者验证
/// </summary>
/// <param name="context">The context.</param>
/// <returns></returns>
public virtual Task IsActiveAsync(IsActiveContext context)
{
Logger.LogDebug("IsActive called from: {caller}", context.Caller);
var user = GetUserById(context.Subject.GetSubjectId());
context.IsActive = user?.Status == 0;
return Task.CompletedTask;
}
private SysUser GetUserById(string id)
{
SysUser sysUser;
if (id == Define.SYSTEM_USERNAME)
{
sysUser = new SysUser
{
Account = Define.SYSTEM_USERNAME,
Id = Define.SYSTEM_USERNAME,
Name = Define.SYSTEM_USERNAME
};
}
else
{
sysUser = UserManager.Get(id);
}
return sysUser;
}
}
}

View File

@@ -0,0 +1,129 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using OpenIddict.Server;
using static OpenIddict.Server.OpenIddictServerEvents;
namespace OpenAuth.IdentityServer.Handlers
{
/// <summary>
/// 降级模式下的自定义授权请求验证 handler
/// 验证 client_id 和 redirect_uri 是否合法
/// </summary>
public class ValidateAuthorizationRequestHandler : IOpenIddictServerHandler<ValidateAuthorizationRequestContext>
{
/// <summary>
/// 已注册的客户端配置
/// </summary>
private static readonly Dictionary<string, ClientConfig> Clients = new(StringComparer.OrdinalIgnoreCase)
{
["OpenAuth.WebApi"] = new ClientConfig
{
RedirectUris = new[]
{
"http://localhost:52789",
"http://localhost:52789/swagger/oauth2-redirect.html",
"http://localhost:1803",
"http://localhost:1803/callback",
"http://localhost:1803/oidc-callback",
},
AllowedScopes = new[] { "openid", "profile", "openauthapi" }
},
["OpenAuth.Pro"] = new ClientConfig
{
RedirectUris = new[]
{
"http://localhost:1803",
"http://localhost:1803/callback",
"http://localhost:1803/oidc-callback",
"http://localhost:1803/oidc-callback.html",
},
AllowedScopes = new[] { "openid", "profile", "openauthapi" }
},
["OpenAuth.Mvc"] = new ClientConfig
{
RedirectUris = new[]
{
"http://localhost:5001",
"http://localhost:5001/signin-oidc",
},
AllowedScopes = new[] { "openid", "profile", "openauthapi" }
}
};
public ValueTask HandleAsync(ValidateAuthorizationRequestContext context)
{
// 验证 client_id
if (string.IsNullOrEmpty(context.ClientId) || !Clients.ContainsKey(context.ClientId))
{
context.Reject(
error: OpenIddict.Abstractions.OpenIddictConstants.Errors.InvalidClient,
description: "未注册的客户端。");
return default;
}
var client = Clients[context.ClientId];
// 验证 redirect_uri如果提供了
if (!string.IsNullOrEmpty(context.RedirectUri))
{
var isValidRedirectUri = client.RedirectUris.Any(uri =>
context.RedirectUri.StartsWith(uri, StringComparison.OrdinalIgnoreCase));
if (!isValidRedirectUri)
{
context.Reject(
error: OpenIddict.Abstractions.OpenIddictConstants.Errors.InvalidClient,
description: "无效的 redirect_uri。");
return default;
}
}
return default;
}
private class ClientConfig
{
public string[] RedirectUris { get; set; } = Array.Empty<string>();
public string[] AllowedScopes { get; set; } = Array.Empty<string>();
}
}
/// <summary>
/// 降级模式下的自定义 Token 请求验证 handler
/// </summary>
public class ValidateTokenRequestHandler : IOpenIddictServerHandler<ValidateTokenRequestContext>
{
public ValueTask HandleAsync(ValidateTokenRequestContext context)
{
// 在降级模式下,授权码流程的 token 请求不需要 client_secret公开客户端
// 只需验证 client_id 是否已注册
if (!string.IsNullOrEmpty(context.ClientId))
{
var knownClients = new[] { "OpenAuth.WebApi", "OpenAuth.Pro", "OpenAuth.Mvc" };
if (!knownClients.Contains(context.ClientId, StringComparer.OrdinalIgnoreCase))
{
context.Reject(
error: OpenIddict.Abstractions.OpenIddictConstants.Errors.InvalidClient,
description: "未注册的客户端。");
return default;
}
}
return default;
}
}
/// <summary>
/// 降级模式下的自定义 Logout 请求验证 handler
/// </summary>
public class ValidateLogoutRequestHandler : IOpenIddictServerHandler<ValidateLogoutRequestContext>
{
public ValueTask HandleAsync(ValidateLogoutRequestContext context)
{
// 允许所有登出请求(可根据需要验证 post_logout_redirect_uri
return default;
}
}
}

View File

@@ -9,7 +9,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="IdentityServer4" Version="3.0.1" />
<PackageReference Include="OpenIddict.AspNetCore" Version="5.8.0" />
<PackageReference Include="Serilog.AspNetCore" Version="3.1.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="3.1.1" />
</ItemGroup>

View File

@@ -1,8 +1,4 @@
// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
using System;
using System;
using Autofac.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;
@@ -16,7 +12,7 @@ namespace OpenAuth.IdentityServer
{
public static void Main(string[] args)
{
Console.Title = "IdentityServer4";
Console.Title = "OpenAuth.IdentityServer (OpenIddict)";
CreateWebHostBuilder(args).Build().Run();
}
@@ -24,7 +20,7 @@ namespace OpenAuth.IdentityServer
public static IHostBuilder CreateWebHostBuilder(string[] args)
{
return Host.CreateDefaultBuilder(args)
.UseServiceProviderFactory(new AutofacServiceProviderFactory()) //将默认ServiceProviderFactory指定为AutofacServiceProviderFactory
.UseServiceProviderFactory(new AutofacServiceProviderFactory())
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseUrls("http://*:12796").UseStartup<Startup>();
@@ -40,4 +36,4 @@ namespace OpenAuth.IdentityServer
});
}
}
}
}

View File

@@ -1,20 +1,10 @@
// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
using System;
using System.Linq;
using System.Collections.Generic;
using System.Security.Claims;
using System.Threading.Tasks;
using IdentityModel;
using IdentityServer4.Events;
using IdentityServer4.Extensions;
using IdentityServer4.Models;
using IdentityServer4.Services;
using IdentityServer4.Stores;
using Infrastructure;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using OpenAuth.App;
using OpenAuth.Repository.Domain;
@@ -22,87 +12,48 @@ using OpenAuth.Repository.Domain;
namespace OpenAuth.IdentityServer.Quickstart.Account
{
/// <summary>
/// This sample controller implements a typical login/logout/provision workflow for local and external accounts.
/// The login service encapsulates the interactions with the user data store. This data store is in-memory only and cannot be used for production!
/// The interaction service provides a way for the UI to communicate with identityserver for validation and context retrieval
/// 处理用户登录/登出流程
/// </summary>
[SecurityHeaders]
[AllowAnonymous]
public class AccountController : Controller
{
private readonly UserManagerApp _userManager;
private readonly IIdentityServerInteractionService _interaction;
private readonly IClientStore _clientStore;
private readonly IAuthenticationSchemeProvider _schemeProvider;
private readonly IEventService _events;
public AccountController(
IIdentityServerInteractionService interaction,
IClientStore clientStore,
IAuthenticationSchemeProvider schemeProvider,
IEventService events, UserManagerApp userManager)
public AccountController(UserManagerApp userManager)
{
_interaction = interaction;
_clientStore = clientStore;
_schemeProvider = schemeProvider;
_events = events;
_userManager = userManager;
}
/// <summary>
/// Entry point into the login workflow
/// 显示登录页面
/// </summary>
[HttpGet]
public async Task<IActionResult> Login(string returnUrl)
public IActionResult Login(string returnUrl)
{
// build a model so we know what to show on the login page
var vm = await BuildLoginViewModelAsync(returnUrl);
if (vm.IsExternalLoginOnly)
var vm = new LoginViewModel
{
// we only have one option for logging in and it's an external provider
return RedirectToAction("Challenge", "External", new { provider = vm.ExternalLoginScheme, returnUrl });
}
ReturnUrl = returnUrl,
EnableLocalLogin = true,
AllowRememberLogin = AccountOptions.AllowRememberLogin
};
return View(vm);
}
/// <summary>
/// Handle postback from username/password login
/// 处理登录表单提交
/// </summary>
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Login(LoginInputModel model, string button)
{
// check if we are in the context of an authorization request
var context = await _interaction.GetAuthorizationContextAsync(model.ReturnUrl);
// the user clicked the "cancel" button
if (button != "login")
{
if (context != null)
// 用户取消登录
if (!string.IsNullOrEmpty(model.ReturnUrl))
{
// if the user cancels, send a result back into IdentityServer as if they
// denied the consent (even if this client does not require consent).
// this will send back an access denied OIDC error response to the client.
await _interaction.GrantConsentAsync(context, ConsentResponse.Denied);
// we can trust model.ReturnUrl since GetAuthorizationContextAsync returned non-null
if (await _clientStore.IsPkceClientAsync(context.ClientId))
{
// if the client is PKCE then we assume it's native, so this change in how to
// return the response is for better UX for the end user.
return View("Redirect", new RedirectViewModel { RedirectUrl = model.ReturnUrl });
}
return Redirect(model.ReturnUrl);
}
else
{
// since we don't have a valid context, then we just go back to the home page
return Redirect("~/");
}
return Redirect("~/");
}
if (ModelState.IsValid)
@@ -114,7 +65,9 @@ namespace OpenAuth.IdentityServer.Quickstart.Account
{
Account = Define.SYSTEM_USERNAME,
Password = Define.SYSTEM_USERPWD,
Id = Define.SYSTEM_USERNAME
Id = Define.SYSTEM_USERNAME,
Name = Define.SYSTEM_USERNAME,
Status = 0
};
}
else
@@ -122,123 +75,63 @@ namespace OpenAuth.IdentityServer.Quickstart.Account
sysUser = _userManager.GetByAccount(model.Username);
}
if (sysUser != null &&(sysUser.Password ==model.Password))
if (sysUser != null && sysUser.Password == model.Password)
{
if (sysUser.Status != 0) //判断用户状态
if (sysUser.Status != 0)
{
await _events.RaiseAsync(new UserLoginFailureEvent(model.Username, "invalid user status"));
ModelState.AddModelError(string.Empty, "user.status must be 0");
var err = await BuildLoginViewModelAsync(model);
return View(err);
ModelState.AddModelError(string.Empty, "用户状态异常,无法登录");
return View(BuildLoginViewModel(model));
}
await _events.RaiseAsync(new UserLoginSuccessEvent(sysUser.Account, sysUser.Id, sysUser.Account));
// only set explicit expiration here if user chooses "remember me".
// otherwise we rely upon expiration configured in cookie middleware.
AuthenticationProperties props = null;
if (AccountOptions.AllowRememberLogin && model.RememberLogin)
// 构建 Cookie Claims
var claims = new List<Claim>
{
props = new AuthenticationProperties
{
IsPersistent = true,
ExpiresUtc = DateTimeOffset.UtcNow.Add(AccountOptions.RememberMeLoginDuration)
};
new Claim(ClaimTypes.NameIdentifier, sysUser.Id),
new Claim(ClaimTypes.Name, sysUser.Account),
};
// issue authentication cookie with subject ID and username
await HttpContext.SignInAsync(sysUser.Id, sysUser.Account, props);
var identity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
var principal = new ClaimsPrincipal(identity);
if (context != null)
var props = new AuthenticationProperties
{
if (await _clientStore.IsPkceClientAsync(context.ClientId))
{
// if the client is PKCE then we assume it's native, so this change in how to
// return the response is for better UX for the end user.
return View("Redirect", new RedirectViewModel { RedirectUrl = model.ReturnUrl });
}
IsPersistent = model.RememberLogin,
};
// we can trust model.ReturnUrl since GetAuthorizationContextAsync returned non-null
// 写入 Cookie
await HttpContext.SignInAsync(
CookieAuthenticationDefaults.AuthenticationScheme,
principal,
props);
// 登录成功后重定向
if (!string.IsNullOrEmpty(model.ReturnUrl) && Url.IsLocalUrl(model.ReturnUrl))
{
return Redirect(model.ReturnUrl);
}
// request for a local page
if (Url.IsLocalUrl(model.ReturnUrl))
{
return Redirect(model.ReturnUrl);
}
else if (string.IsNullOrEmpty(model.ReturnUrl))
{
return Redirect("~/");
}
else
{
// user might have clicked on a malicious link - should be logged
throw new Exception("invalid return URL");
}
return Redirect("~/");
}
await _events.RaiseAsync(new UserLoginFailureEvent(model.Username, "invalid credentials", clientId:context?.ClientId));
ModelState.AddModelError(string.Empty, AccountOptions.InvalidCredentialsErrorMessage);
}
// something went wrong, show form with error
var vm = await BuildLoginViewModelAsync(model);
return View(vm);
// 登录失败,重新显示表单
return View(BuildLoginViewModel(model));
}
/// <summary>
/// Show logout page
/// 显示登出页面
/// </summary>
[HttpGet]
public async Task<IActionResult> Logout(string logoutId)
{
// build a model so the logout page knows what to display
var vm = await BuildLogoutViewModelAsync(logoutId);
if (vm.ShowLogoutPrompt == false)
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
return View("LoggedOut", new LoggedOutViewModel
{
// if the request for logout was properly authenticated from IdentityServer, then
// we don't need to show the prompt and can just log the user out directly.
return await Logout(vm);
}
return View(vm);
}
/// <summary>
/// Handle logout page postback
/// </summary>
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Logout(LogoutInputModel model)
{
// build a model so the logged out page knows what to display
var vm = await BuildLoggedOutViewModelAsync(model.LogoutId);
if (User?.Identity.IsAuthenticated == true)
{
// delete local authentication cookie
await HttpContext.SignOutAsync();
// raise the logout event
await _events.RaiseAsync(new UserLogoutSuccessEvent(User.GetSubjectId(), User.GetDisplayName()));
}
// check if we need to trigger sign-out at an upstream identity provider
if (vm.TriggerExternalSignout)
{
// build a return URL so the upstream provider will redirect back
// to us after the user has logged out. this allows us to then
// complete our single sign-out processing.
string url = Url.Action("Logout", new { logoutId = vm.LogoutId });
// this triggers a redirect to the external provider for sign-out
return SignOut(new AuthenticationProperties { RedirectUri = url }, vm.ExternalAuthenticationScheme);
}
return View("LoggedOut", vm);
AutomaticRedirectAfterSignOut = true,
PostLogoutRedirectUri = "/"
});
}
[HttpGet]
@@ -247,138 +140,16 @@ namespace OpenAuth.IdentityServer.Quickstart.Account
return View();
}
/*****************************************/
/* helper APIs for the AccountController */
/*****************************************/
private async Task<LoginViewModel> BuildLoginViewModelAsync(string returnUrl)
private LoginViewModel BuildLoginViewModel(LoginInputModel model)
{
var context = await _interaction.GetAuthorizationContextAsync(returnUrl);
if (context?.IdP != null && await _schemeProvider.GetSchemeAsync(context.IdP) != null)
{
var local = context.IdP == IdentityServer4.IdentityServerConstants.LocalIdentityProvider;
// this is meant to short circuit the UI and only trigger the one external IdP
var vm = new LoginViewModel
{
EnableLocalLogin = local,
ReturnUrl = returnUrl,
Username = context?.LoginHint,
};
if (!local)
{
vm.ExternalProviders = new[] { new ExternalProvider { AuthenticationScheme = context.IdP } };
}
return vm;
}
var schemes = await _schemeProvider.GetAllSchemesAsync();
var providers = schemes
.Where(x => x.DisplayName != null ||
(x.Name.Equals(AccountOptions.WindowsAuthenticationSchemeName, StringComparison.OrdinalIgnoreCase))
)
.Select(x => new ExternalProvider
{
DisplayName = x.DisplayName,
AuthenticationScheme = x.Name
}).ToList();
var allowLocal = true;
if (context?.ClientId != null)
{
var client = await _clientStore.FindEnabledClientByIdAsync(context.ClientId);
if (client != null)
{
allowLocal = client.EnableLocalLogin;
if (client.IdentityProviderRestrictions != null && client.IdentityProviderRestrictions.Any())
{
providers = providers.Where(provider => client.IdentityProviderRestrictions.Contains(provider.AuthenticationScheme)).ToList();
}
}
}
return new LoginViewModel
{
AllowRememberLogin = AccountOptions.AllowRememberLogin,
EnableLocalLogin = allowLocal && AccountOptions.AllowLocalLogin,
ReturnUrl = returnUrl,
Username = context?.LoginHint,
ExternalProviders = providers.ToArray()
Username = model.Username,
RememberLogin = model.RememberLogin,
ReturnUrl = model.ReturnUrl,
EnableLocalLogin = true,
AllowRememberLogin = AccountOptions.AllowRememberLogin
};
}
private async Task<LoginViewModel> BuildLoginViewModelAsync(LoginInputModel model)
{
var vm = await BuildLoginViewModelAsync(model.ReturnUrl);
vm.Username = model.Username;
vm.RememberLogin = model.RememberLogin;
return vm;
}
private async Task<LogoutViewModel> BuildLogoutViewModelAsync(string logoutId)
{
var vm = new LogoutViewModel { LogoutId = logoutId, ShowLogoutPrompt = AccountOptions.ShowLogoutPrompt };
if (User?.Identity.IsAuthenticated != true)
{
// if the user is not authenticated, then just show logged out page
vm.ShowLogoutPrompt = false;
return vm;
}
var context = await _interaction.GetLogoutContextAsync(logoutId);
if (context?.ShowSignoutPrompt == false)
{
// it's safe to automatically sign-out
vm.ShowLogoutPrompt = false;
return vm;
}
// show the logout prompt. this prevents attacks where the user
// is automatically signed out by another malicious web page.
return vm;
}
private async Task<LoggedOutViewModel> BuildLoggedOutViewModelAsync(string logoutId)
{
// get context information (client name, post logout redirect URI and iframe for federated signout)
var logout = await _interaction.GetLogoutContextAsync(logoutId);
var vm = new LoggedOutViewModel
{
AutomaticRedirectAfterSignOut = AccountOptions.AutomaticRedirectAfterSignOut,
PostLogoutRedirectUri = logout?.PostLogoutRedirectUri,
ClientName = string.IsNullOrEmpty(logout?.ClientName) ? logout?.ClientId : logout?.ClientName,
SignOutIframeUrl = logout?.SignOutIFrameUrl,
LogoutId = logoutId
};
if (User?.Identity.IsAuthenticated == true)
{
var idp = User.FindFirst(JwtClaimTypes.IdentityProvider)?.Value;
if (idp != null && idp != IdentityServer4.IdentityServerConstants.LocalIdentityProvider)
{
var providerSupportsSignout = await HttpContext.GetSchemeSupportsSignOutAsync(idp);
if (providerSupportsSignout)
{
if (vm.LogoutId == null)
{
// if there's no current logout context, we need to create one
// this captures necessary info from the current logged in user
// before we signout and redirect away to the external IdP for signout
vm.LogoutId = await _interaction.CreateLogoutContextAsync();
}
vm.ExternalAuthenticationScheme = idp;
}
}
}
return vm;
}
}
}

View File

@@ -1,251 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Security.Principal;
using System.Threading.Tasks;
using IdentityModel;
using IdentityServer4.Events;
using IdentityServer4.Services;
using IdentityServer4.Stores;
using IdentityServer4.Test;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
namespace OpenAuth.IdentityServer.Quickstart.Account
{
[SecurityHeaders]
[AllowAnonymous]
public class ExternalController : Controller
{
private readonly TestUserStore _users;
private readonly IIdentityServerInteractionService _interaction;
private readonly IClientStore _clientStore;
private readonly ILogger<ExternalController> _logger;
private readonly IEventService _events;
public ExternalController(
IIdentityServerInteractionService interaction,
IClientStore clientStore,
IEventService events,
ILogger<ExternalController> logger,
TestUserStore users = null)
{
// if the TestUserStore is not in DI, then we'll just use the global users collection
// this is where you would plug in your own custom identity management library (e.g. ASP.NET Identity)
_users = users ?? new TestUserStore(TestUsers.Users);
_interaction = interaction;
_clientStore = clientStore;
_logger = logger;
_events = events;
}
/// <summary>
/// initiate roundtrip to external authentication provider
/// </summary>
[HttpGet]
public async Task<IActionResult> Challenge(string provider, string returnUrl)
{
if (string.IsNullOrEmpty(returnUrl)) returnUrl = "~/";
// validate returnUrl - either it is a valid OIDC URL or back to a local page
if (Url.IsLocalUrl(returnUrl) == false && _interaction.IsValidReturnUrl(returnUrl) == false)
{
// user might have clicked on a malicious link - should be logged
throw new Exception("invalid return URL");
}
if (AccountOptions.WindowsAuthenticationSchemeName == provider)
{
// windows authentication needs special handling
return await ProcessWindowsLoginAsync(returnUrl);
}
else
{
// start challenge and roundtrip the return URL and scheme
var props = new AuthenticationProperties
{
RedirectUri = Url.Action(nameof(Callback)),
Items =
{
{ "returnUrl", returnUrl },
{ "scheme", provider },
}
};
return Challenge(props, provider);
}
}
/// <summary>
/// Post processing of external authentication
/// </summary>
[HttpGet]
public async Task<IActionResult> Callback()
{
// read external identity from the temporary cookie
var result = await HttpContext.AuthenticateAsync(IdentityServer4.IdentityServerConstants.ExternalCookieAuthenticationScheme);
if (result?.Succeeded != true)
{
throw new Exception("External authentication error");
}
if (_logger.IsEnabled(LogLevel.Debug))
{
var externalClaims = result.Principal.Claims.Select(c => $"{c.Type}: {c.Value}");
_logger.LogDebug("External claims: {@claims}", externalClaims);
}
// lookup our user and external provider info
var (user, provider, providerUserId, claims) = FindUserFromExternalProvider(result);
if (user == null)
{
// this might be where you might initiate a custom workflow for user registration
// in this sample we don't show how that would be done, as our sample implementation
// simply auto-provisions new external user
user = AutoProvisionUser(provider, providerUserId, claims);
}
// this allows us to collect any additonal claims or properties
// for the specific prtotocols used and store them in the local auth cookie.
// this is typically used to store data needed for signout from those protocols.
var additionalLocalClaims = new List<Claim>();
var localSignInProps = new AuthenticationProperties();
ProcessLoginCallbackForOidc(result, additionalLocalClaims, localSignInProps);
ProcessLoginCallbackForWsFed(result, additionalLocalClaims, localSignInProps);
ProcessLoginCallbackForSaml2p(result, additionalLocalClaims, localSignInProps);
// issue authentication cookie for user
await HttpContext.SignInAsync(user.SubjectId, user.Username, provider, localSignInProps, additionalLocalClaims.ToArray());
// delete temporary cookie used during external authentication
await HttpContext.SignOutAsync(IdentityServer4.IdentityServerConstants.ExternalCookieAuthenticationScheme);
// retrieve return URL
var returnUrl = result.Properties.Items["returnUrl"] ?? "~/";
// check if external login is in the context of an OIDC request
var context = await _interaction.GetAuthorizationContextAsync(returnUrl);
await _events.RaiseAsync(new UserLoginSuccessEvent(provider, providerUserId, user.SubjectId, user.Username, true, context?.ClientId));
if (context != null)
{
if (await _clientStore.IsPkceClientAsync(context.ClientId))
{
// if the client is PKCE then we assume it's native, so this change in how to
// return the response is for better UX for the end user.
return View("Redirect", new RedirectViewModel { RedirectUrl = returnUrl });
}
}
return Redirect(returnUrl);
}
private async Task<IActionResult> ProcessWindowsLoginAsync(string returnUrl)
{
// see if windows auth has already been requested and succeeded
var result = await HttpContext.AuthenticateAsync(AccountOptions.WindowsAuthenticationSchemeName);
if (result?.Principal is WindowsPrincipal wp)
{
// we will issue the external cookie and then redirect the
// user back to the external callback, in essence, treating windows
// auth the same as any other external authentication mechanism
var props = new AuthenticationProperties()
{
RedirectUri = Url.Action("Callback"),
Items =
{
{ "returnUrl", returnUrl },
{ "scheme", AccountOptions.WindowsAuthenticationSchemeName },
}
};
var id = new ClaimsIdentity(AccountOptions.WindowsAuthenticationSchemeName);
id.AddClaim(new Claim(JwtClaimTypes.Subject, wp.Identity.Name));
id.AddClaim(new Claim(JwtClaimTypes.Name, wp.Identity.Name));
// add the groups as claims -- be careful if the number of groups is too large
if (AccountOptions.IncludeWindowsGroups)
{
var wi = wp.Identity as WindowsIdentity;
var groups = wi.Groups.Translate(typeof(NTAccount));
var roles = groups.Select(x => new Claim(JwtClaimTypes.Role, x.Value));
id.AddClaims(roles);
}
await HttpContext.SignInAsync(
IdentityServer4.IdentityServerConstants.ExternalCookieAuthenticationScheme,
new ClaimsPrincipal(id),
props);
return Redirect(props.RedirectUri);
}
else
{
// trigger windows auth
// since windows auth don't support the redirect uri,
// this URL is re-triggered when we call challenge
return Challenge(AccountOptions.WindowsAuthenticationSchemeName);
}
}
private (TestUser user, string provider, string providerUserId, IEnumerable<Claim> claims) FindUserFromExternalProvider(AuthenticateResult result)
{
var externalUser = result.Principal;
// try to determine the unique id of the external user (issued by the provider)
// the most common claim type for that are the sub claim and the NameIdentifier
// depending on the external provider, some other claim type might be used
var userIdClaim = externalUser.FindFirst(JwtClaimTypes.Subject) ??
externalUser.FindFirst(ClaimTypes.NameIdentifier) ??
throw new Exception("Unknown userid");
// remove the user id claim so we don't include it as an extra claim if/when we provision the user
var claims = externalUser.Claims.ToList();
claims.Remove(userIdClaim);
var provider = result.Properties.Items["scheme"];
var providerUserId = userIdClaim.Value;
// find external user
var user = _users.FindByExternalProvider(provider, providerUserId);
return (user, provider, providerUserId, claims);
}
private TestUser AutoProvisionUser(string provider, string providerUserId, IEnumerable<Claim> claims)
{
var user = _users.AutoProvisionUser(provider, providerUserId, claims.ToList());
return user;
}
private void ProcessLoginCallbackForOidc(AuthenticateResult externalResult, List<Claim> localClaims, AuthenticationProperties localSignInProps)
{
// if the external system sent a session id claim, copy it over
// so we can use it for single sign-out
var sid = externalResult.Principal.Claims.FirstOrDefault(x => x.Type == JwtClaimTypes.SessionId);
if (sid != null)
{
localClaims.Add(new Claim(JwtClaimTypes.SessionId, sid.Value));
}
// if the external provider issued an id_token, we'll keep it for signout
var id_token = externalResult.Properties.GetTokenValue("id_token");
if (id_token != null)
{
localSignInProps.StoreTokens(new[] { new AuthenticationToken { Name = "id_token", Value = id_token } });
}
}
private void ProcessLoginCallbackForWsFed(AuthenticateResult externalResult, List<Claim> localClaims, AuthenticationProperties localSignInProps)
{
}
private void ProcessLoginCallbackForSaml2p(AuthenticateResult externalResult, List<Claim> localClaims, AuthenticationProperties localSignInProps)
{
}
}
}

View File

@@ -1,266 +0,0 @@
// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
using System.Linq;
using System.Threading.Tasks;
using IdentityServer4.Events;
using IdentityServer4.Extensions;
using IdentityServer4.Models;
using IdentityServer4.Services;
using IdentityServer4.Stores;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using OpenAuth.IdentityServer.Quickstart.Account;
namespace OpenAuth.IdentityServer.Quickstart.Consent
{
/// <summary>
/// This controller processes the consent UI
/// </summary>
[SecurityHeaders]
[Authorize]
public class ConsentController : Controller
{
private readonly IIdentityServerInteractionService _interaction;
private readonly IClientStore _clientStore;
private readonly IResourceStore _resourceStore;
private readonly IEventService _events;
private readonly ILogger<ConsentController> _logger;
public ConsentController(
IIdentityServerInteractionService interaction,
IClientStore clientStore,
IResourceStore resourceStore,
IEventService events,
ILogger<ConsentController> logger)
{
_interaction = interaction;
_clientStore = clientStore;
_resourceStore = resourceStore;
_events = events;
_logger = logger;
}
/// <summary>
/// Shows the consent screen
/// </summary>
/// <param name="returnUrl"></param>
/// <returns></returns>
[HttpGet]
public async Task<IActionResult> Index(string returnUrl)
{
var vm = await BuildViewModelAsync(returnUrl);
if (vm != null)
{
return View("Index", vm);
}
return View("Error");
}
/// <summary>
/// Handles the consent screen postback
/// </summary>
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Index(ConsentInputModel model)
{
var result = await ProcessConsent(model);
if (result.IsRedirect)
{
if (await _clientStore.IsPkceClientAsync(result.ClientId))
{
// if the client is PKCE then we assume it's native, so this change in how to
// return the response is for better UX for the end user.
return View("Redirect", new RedirectViewModel { RedirectUrl = result.RedirectUri });
}
return Redirect(result.RedirectUri);
}
if (result.HasValidationError)
{
ModelState.AddModelError(string.Empty, result.ValidationError);
}
if (result.ShowView)
{
return View("Index", result.ViewModel);
}
return View("Error");
}
/*****************************************/
/* helper APIs for the ConsentController */
/*****************************************/
private async Task<ProcessConsentResult> ProcessConsent(ConsentInputModel model)
{
var result = new ProcessConsentResult();
// validate return url is still valid
var request = await _interaction.GetAuthorizationContextAsync(model.ReturnUrl);
if (request == null) return result;
ConsentResponse grantedConsent = null;
// user clicked 'no' - send back the standard 'access_denied' response
if (model?.Button == "no")
{
grantedConsent = ConsentResponse.Denied;
// emit event
await _events.RaiseAsync(new ConsentDeniedEvent(User.GetSubjectId(), request.ClientId, request.ScopesRequested));
}
// user clicked 'yes' - validate the data
else if (model?.Button == "yes")
{
// if the user consented to some scope, build the response model
if (model.ScopesConsented != null && model.ScopesConsented.Any())
{
var scopes = model.ScopesConsented;
if (ConsentOptions.EnableOfflineAccess == false)
{
scopes = scopes.Where(x => x != IdentityServer4.IdentityServerConstants.StandardScopes.OfflineAccess);
}
grantedConsent = new ConsentResponse
{
RememberConsent = model.RememberConsent,
ScopesConsented = scopes.ToArray()
};
// emit event
await _events.RaiseAsync(new ConsentGrantedEvent(User.GetSubjectId(), request.ClientId, request.ScopesRequested, grantedConsent.ScopesConsented, grantedConsent.RememberConsent));
}
else
{
result.ValidationError = ConsentOptions.MustChooseOneErrorMessage;
}
}
else
{
result.ValidationError = ConsentOptions.InvalidSelectionErrorMessage;
}
if (grantedConsent != null)
{
// communicate outcome of consent back to identityserver
await _interaction.GrantConsentAsync(request, grantedConsent);
// indicate that's it ok to redirect back to authorization endpoint
result.RedirectUri = model.ReturnUrl;
result.ClientId = request.ClientId;
}
else
{
// we need to redisplay the consent UI
result.ViewModel = await BuildViewModelAsync(model.ReturnUrl, model);
}
return result;
}
private async Task<ConsentViewModel> BuildViewModelAsync(string returnUrl, ConsentInputModel model = null)
{
var request = await _interaction.GetAuthorizationContextAsync(returnUrl);
if (request != null)
{
var client = await _clientStore.FindEnabledClientByIdAsync(request.ClientId);
if (client != null)
{
var resources = await _resourceStore.FindEnabledResourcesByScopeAsync(request.ScopesRequested);
if (resources != null && (resources.IdentityResources.Any() || resources.ApiResources.Any()))
{
return CreateConsentViewModel(model, returnUrl, request, client, resources);
}
else
{
_logger.LogError("No scopes matching: {0}", request.ScopesRequested.Aggregate((x, y) => x + ", " + y));
}
}
else
{
_logger.LogError("Invalid client id: {0}", request.ClientId);
}
}
else
{
_logger.LogError("No consent request matching request: {0}", returnUrl);
}
return null;
}
private ConsentViewModel CreateConsentViewModel(
ConsentInputModel model, string returnUrl,
AuthorizationRequest request,
Client client, Resources resources)
{
var vm = new ConsentViewModel
{
RememberConsent = model?.RememberConsent ?? true,
ScopesConsented = model?.ScopesConsented ?? Enumerable.Empty<string>(),
ReturnUrl = returnUrl,
ClientName = client.ClientName ?? client.ClientId,
ClientUrl = client.ClientUri,
ClientLogoUrl = client.LogoUri,
AllowRememberConsent = client.AllowRememberConsent
};
vm.IdentityScopes = resources.IdentityResources.Select(x => CreateScopeViewModel(x, vm.ScopesConsented.Contains(x.Name) || model == null)).ToArray();
vm.ResourceScopes = resources.ApiResources.SelectMany(x => x.Scopes).Select(x => CreateScopeViewModel(x, vm.ScopesConsented.Contains(x.Name) || model == null)).ToArray();
if (ConsentOptions.EnableOfflineAccess && resources.OfflineAccess)
{
vm.ResourceScopes = vm.ResourceScopes.Union(new ScopeViewModel[] {
GetOfflineAccessScope(vm.ScopesConsented.Contains(IdentityServer4.IdentityServerConstants.StandardScopes.OfflineAccess) || model == null)
});
}
return vm;
}
private ScopeViewModel CreateScopeViewModel(IdentityResource identity, bool check)
{
return new ScopeViewModel
{
Name = identity.Name,
DisplayName = identity.DisplayName,
Description = identity.Description,
Emphasize = identity.Emphasize,
Required = identity.Required,
Checked = check || identity.Required
};
}
public ScopeViewModel CreateScopeViewModel(Scope scope, bool check)
{
return new ScopeViewModel
{
Name = scope.Name,
DisplayName = scope.DisplayName,
Description = scope.Description,
Emphasize = scope.Emphasize,
Required = scope.Required,
Checked = check || scope.Required
};
}
private ScopeViewModel GetOfflineAccessScope(bool check)
{
return new ScopeViewModel
{
Name = IdentityServer4.IdentityServerConstants.StandardScopes.OfflineAccess,
DisplayName = ConsentOptions.OfflineAccessDisplayName,
Description = ConsentOptions.OfflineAccessDescription,
Emphasize = true,
Checked = check
};
}
}
}

View File

@@ -1,16 +0,0 @@
// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
using System.Collections.Generic;
namespace OpenAuth.IdentityServer.Quickstart.Consent
{
public class ConsentInputModel
{
public string Button { get; set; }
public IEnumerable<string> ScopesConsented { get; set; }
public bool RememberConsent { get; set; }
public string ReturnUrl { get; set; }
}
}

View File

@@ -1,16 +0,0 @@
// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
namespace OpenAuth.IdentityServer.Quickstart.Consent
{
public class ConsentOptions
{
public static bool EnableOfflineAccess = true;
public static string OfflineAccessDisplayName = "Offline Access";
public static string OfflineAccessDescription = "Access to your applications and resources, even when you are offline";
public static readonly string MustChooseOneErrorMessage = "You must pick at least one permission";
public static readonly string InvalidSelectionErrorMessage = "Invalid selection";
}
}

View File

@@ -1,19 +0,0 @@
// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
using System.Collections.Generic;
namespace OpenAuth.IdentityServer.Quickstart.Consent
{
public class ConsentViewModel : ConsentInputModel
{
public string ClientName { get; set; }
public string ClientUrl { get; set; }
public string ClientLogoUrl { get; set; }
public bool AllowRememberConsent { get; set; }
public IEnumerable<ScopeViewModel> IdentityScopes { get; set; }
public IEnumerable<ScopeViewModel> ResourceScopes { get; set; }
}
}

View File

@@ -1,19 +0,0 @@
// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
namespace OpenAuth.IdentityServer.Quickstart.Consent
{
public class ProcessConsentResult
{
public bool IsRedirect => RedirectUri != null;
public string RedirectUri { get; set; }
public string ClientId { get; set; }
public bool ShowView => ViewModel != null;
public ConsentViewModel ViewModel { get; set; }
public bool HasValidationError => ValidationError != null;
public string ValidationError { get; set; }
}
}

View File

@@ -1,16 +0,0 @@
// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
namespace OpenAuth.IdentityServer.Quickstart.Consent
{
public class ScopeViewModel
{
public string Name { get; set; }
public string DisplayName { get; set; }
public string Description { get; set; }
public bool Emphasize { get; set; }
public bool Required { get; set; }
public bool Checked { get; set; }
}
}

View File

@@ -1,13 +0,0 @@
// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
using OpenAuth.IdentityServer.Quickstart.Consent;
namespace OpenAuth.IdentityServer.Quickstart.Device
{
public class DeviceAuthorizationInputModel : ConsentInputModel
{
public string UserCode { get; set; }
}
}

View File

@@ -1,14 +0,0 @@
// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
using OpenAuth.IdentityServer.Quickstart.Consent;
namespace OpenAuth.IdentityServer.Quickstart.Device
{
public class DeviceAuthorizationViewModel : ConsentViewModel
{
public string UserCode { get; set; }
public bool ConfirmUserCode { get; set; }
}
}

View File

@@ -1,243 +0,0 @@
// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
using System;
using System.Linq;
using System.Threading.Tasks;
using IdentityServer4.Configuration;
using IdentityServer4.Events;
using IdentityServer4.Extensions;
using IdentityServer4.Models;
using IdentityServer4.Services;
using IdentityServer4.Stores;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using OpenAuth.IdentityServer.Quickstart.Consent;
namespace OpenAuth.IdentityServer.Quickstart.Device
{
[Authorize]
[SecurityHeaders]
public class DeviceController : Controller
{
private readonly IDeviceFlowInteractionService _interaction;
private readonly IClientStore _clientStore;
private readonly IResourceStore _resourceStore;
private readonly IEventService _events;
private readonly IOptions<IdentityServerOptions> _options;
private readonly ILogger<DeviceController> _logger;
public DeviceController(
IDeviceFlowInteractionService interaction,
IClientStore clientStore,
IResourceStore resourceStore,
IEventService eventService,
IOptions<IdentityServerOptions> options,
ILogger<DeviceController> logger)
{
_interaction = interaction;
_clientStore = clientStore;
_resourceStore = resourceStore;
_events = eventService;
_options = options;
_logger = logger;
}
[HttpGet]
public async Task<IActionResult> Index()
{
string userCodeParamName = _options.Value.UserInteraction.DeviceVerificationUserCodeParameter;
string userCode = Request.Query[userCodeParamName];
if (string.IsNullOrWhiteSpace(userCode)) return View("UserCodeCapture");
var vm = await BuildViewModelAsync(userCode);
if (vm == null) return View("Error");
vm.ConfirmUserCode = true;
return View("UserCodeConfirmation", vm);
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> UserCodeCapture(string userCode)
{
var vm = await BuildViewModelAsync(userCode);
if (vm == null) return View("Error");
return View("UserCodeConfirmation", vm);
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Callback(DeviceAuthorizationInputModel model)
{
if (model == null) throw new ArgumentNullException(nameof(model));
var result = await ProcessConsent(model);
if (result.HasValidationError) return View("Error");
return View("Success");
}
private async Task<ProcessConsentResult> ProcessConsent(DeviceAuthorizationInputModel model)
{
var result = new ProcessConsentResult();
var request = await _interaction.GetAuthorizationContextAsync(model.UserCode);
if (request == null) return result;
ConsentResponse grantedConsent = null;
// user clicked 'no' - send back the standard 'access_denied' response
if (model.Button == "no")
{
grantedConsent = ConsentResponse.Denied;
// emit event
await _events.RaiseAsync(new ConsentDeniedEvent(User.GetSubjectId(), request.ClientId, request.ScopesRequested));
}
// user clicked 'yes' - validate the data
else if (model.Button == "yes")
{
// if the user consented to some scope, build the response model
if (model.ScopesConsented != null && model.ScopesConsented.Any())
{
var scopes = model.ScopesConsented;
if (ConsentOptions.EnableOfflineAccess == false)
{
scopes = scopes.Where(x => x != IdentityServer4.IdentityServerConstants.StandardScopes.OfflineAccess);
}
grantedConsent = new ConsentResponse
{
RememberConsent = model.RememberConsent,
ScopesConsented = scopes.ToArray()
};
// emit event
await _events.RaiseAsync(new ConsentGrantedEvent(User.GetSubjectId(), request.ClientId, request.ScopesRequested, grantedConsent.ScopesConsented, grantedConsent.RememberConsent));
}
else
{
result.ValidationError = ConsentOptions.MustChooseOneErrorMessage;
}
}
else
{
result.ValidationError = ConsentOptions.InvalidSelectionErrorMessage;
}
if (grantedConsent != null)
{
// communicate outcome of consent back to identityserver
await _interaction.HandleRequestAsync(model.UserCode, grantedConsent);
// indicate that's it ok to redirect back to authorization endpoint
result.RedirectUri = model.ReturnUrl;
result.ClientId = request.ClientId;
}
else
{
// we need to redisplay the consent UI
result.ViewModel = await BuildViewModelAsync(model.UserCode, model);
}
return result;
}
private async Task<DeviceAuthorizationViewModel> BuildViewModelAsync(string userCode, DeviceAuthorizationInputModel model = null)
{
var request = await _interaction.GetAuthorizationContextAsync(userCode);
if (request != null)
{
var client = await _clientStore.FindEnabledClientByIdAsync(request.ClientId);
if (client != null)
{
var resources = await _resourceStore.FindEnabledResourcesByScopeAsync(request.ScopesRequested);
if (resources != null && (resources.IdentityResources.Any() || resources.ApiResources.Any()))
{
return CreateConsentViewModel(userCode, model, client, resources);
}
else
{
_logger.LogError("No scopes matching: {0}", request.ScopesRequested.Aggregate((x, y) => x + ", " + y));
}
}
else
{
_logger.LogError("Invalid client id: {0}", request.ClientId);
}
}
return null;
}
private DeviceAuthorizationViewModel CreateConsentViewModel(string userCode, DeviceAuthorizationInputModel model, Client client, Resources resources)
{
var vm = new DeviceAuthorizationViewModel
{
UserCode = userCode,
RememberConsent = model?.RememberConsent ?? true,
ScopesConsented = model?.ScopesConsented ?? Enumerable.Empty<string>(),
ClientName = client.ClientName ?? client.ClientId,
ClientUrl = client.ClientUri,
ClientLogoUrl = client.LogoUri,
AllowRememberConsent = client.AllowRememberConsent
};
vm.IdentityScopes = resources.IdentityResources.Select(x => CreateScopeViewModel(x, vm.ScopesConsented.Contains(x.Name) || model == null)).ToArray();
vm.ResourceScopes = resources.ApiResources.SelectMany(x => x.Scopes).Select(x => CreateScopeViewModel(x, vm.ScopesConsented.Contains(x.Name) || model == null)).ToArray();
if (ConsentOptions.EnableOfflineAccess && resources.OfflineAccess)
{
vm.ResourceScopes = vm.ResourceScopes.Union(new[]
{
GetOfflineAccessScope(vm.ScopesConsented.Contains(IdentityServer4.IdentityServerConstants.StandardScopes.OfflineAccess) || model == null)
});
}
return vm;
}
private ScopeViewModel CreateScopeViewModel(IdentityResource identity, bool check)
{
return new ScopeViewModel
{
Name = identity.Name,
DisplayName = identity.DisplayName,
Description = identity.Description,
Emphasize = identity.Emphasize,
Required = identity.Required,
Checked = check || identity.Required
};
}
public ScopeViewModel CreateScopeViewModel(Scope scope, bool check)
{
return new ScopeViewModel
{
Name = scope.Name,
DisplayName = scope.DisplayName,
Description = scope.Description,
Emphasize = scope.Emphasize,
Required = scope.Required,
Checked = check || scope.Required
};
}
private ScopeViewModel GetOfflineAccessScope(bool check)
{
return new ScopeViewModel
{
Name = IdentityServer4.IdentityServerConstants.StandardScopes.OfflineAccess,
DisplayName = ConsentOptions.OfflineAccessDisplayName,
Description = ConsentOptions.OfflineAccessDescription,
Emphasize = true,
Checked = check
};
}
}
}

View File

@@ -1,29 +0,0 @@
// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace OpenAuth.IdentityServer.Quickstart.Diagnostics
{
[SecurityHeaders]
[Authorize]
public class DiagnosticsController : Controller
{
public async Task<IActionResult> Index()
{
var localAddresses = new string[] { "127.0.0.1", "::1", HttpContext.Connection.LocalIpAddress.ToString() };
if (!localAddresses.Contains(HttpContext.Connection.RemoteIpAddress.ToString()))
{
return NotFound();
}
var model = new DiagnosticsViewModel(await HttpContext.AuthenticateAsync());
return View(model);
}
}
}

View File

@@ -1,32 +0,0 @@
// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
using System.Collections.Generic;
using System.Text;
using IdentityModel;
using Microsoft.AspNetCore.Authentication;
using Newtonsoft.Json;
namespace OpenAuth.IdentityServer.Quickstart.Diagnostics
{
public class DiagnosticsViewModel
{
public DiagnosticsViewModel(AuthenticateResult result)
{
AuthenticateResult = result;
if (result.Properties.Items.ContainsKey("client_list"))
{
var encoded = result.Properties.Items["client_list"];
var bytes = Base64Url.Decode(encoded);
var value = Encoding.UTF8.GetString(bytes);
Clients = JsonConvert.DeserializeObject<string[]>(value);
}
}
public AuthenticateResult AuthenticateResult { get; }
public IEnumerable<string> Clients { get; } = new List<string>();
}
}

View File

@@ -1,25 +0,0 @@
using System.Threading.Tasks;
using IdentityServer4.Stores;
namespace OpenAuth.IdentityServer.Quickstart
{
public static class Extensions
{
/// <summary>
/// Determines whether the client is configured to use PKCE.
/// </summary>
/// <param name="store">The store.</param>
/// <param name="client_id">The client identifier.</param>
/// <returns></returns>
public static async Task<bool> IsPkceClientAsync(this IClientStore store, string client_id)
{
if (!string.IsNullOrWhiteSpace(client_id))
{
var client = await store.FindEnabledClientByIdAsync(client_id);
return client?.RequirePkce == true;
}
return false;
}
}
}

View File

@@ -1,96 +0,0 @@
// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using IdentityServer4.Events;
using IdentityServer4.Extensions;
using IdentityServer4.Services;
using IdentityServer4.Stores;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace OpenAuth.IdentityServer.Quickstart.Grants
{
/// <summary>
/// This sample controller allows a user to revoke grants given to clients
/// </summary>
[SecurityHeaders]
[Authorize]
public class GrantsController : Controller
{
private readonly IIdentityServerInteractionService _interaction;
private readonly IClientStore _clients;
private readonly IResourceStore _resources;
private readonly IEventService _events;
public GrantsController(IIdentityServerInteractionService interaction,
IClientStore clients,
IResourceStore resources,
IEventService events)
{
_interaction = interaction;
_clients = clients;
_resources = resources;
_events = events;
}
/// <summary>
/// Show list of grants
/// </summary>
[HttpGet]
public async Task<IActionResult> Index()
{
return View("Index", await BuildViewModelAsync());
}
/// <summary>
/// Handle postback to revoke a client
/// </summary>
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Revoke(string clientId)
{
await _interaction.RevokeUserConsentAsync(clientId);
await _events.RaiseAsync(new GrantsRevokedEvent(User.GetSubjectId(), clientId));
return RedirectToAction("Index");
}
private async Task<GrantsViewModel> BuildViewModelAsync()
{
var grants = await _interaction.GetAllUserConsentsAsync();
var list = new List<GrantViewModel>();
foreach(var grant in grants)
{
var client = await _clients.FindClientByIdAsync(grant.ClientId);
if (client != null)
{
var resources = await _resources.FindResourcesByScopeAsync(grant.Scopes);
var item = new GrantViewModel()
{
ClientId = client.ClientId,
ClientName = client.ClientName ?? client.ClientId,
ClientLogoUrl = client.LogoUri,
ClientUrl = client.ClientUri,
Created = grant.CreationTime,
Expires = grant.Expiration,
IdentityGrantNames = resources.IdentityResources.Select(x => x.DisplayName ?? x.Name).ToArray(),
ApiGrantNames = resources.ApiResources.Select(x => x.DisplayName ?? x.Name).ToArray()
};
list.Add(item);
}
}
return new GrantsViewModel
{
Grants = list
};
}
}
}

View File

@@ -1,26 +0,0 @@
// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
using System;
using System.Collections.Generic;
namespace OpenAuth.IdentityServer.Quickstart.Grants
{
public class GrantsViewModel
{
public IEnumerable<GrantViewModel> Grants { get; set; }
}
public class GrantViewModel
{
public string ClientId { get; set; }
public string ClientName { get; set; }
public string ClientUrl { get; set; }
public string ClientLogoUrl { get; set; }
public DateTime Created { get; set; }
public DateTime? Expires { get; set; }
public IEnumerable<string> IdentityGrantNames { get; set; }
public IEnumerable<string> ApiGrantNames { get; set; }
}
}

View File

@@ -1,22 +1,8 @@
// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
using IdentityServer4.Models;
namespace OpenAuth.IdentityServer.Quickstart.Home
namespace OpenAuth.IdentityServer.Quickstart.Home
{
public class ErrorViewModel
{
public ErrorViewModel()
{
}
public ErrorViewModel(string error)
{
Error = new ErrorMessage { Error = error };
}
public ErrorMessage Error { get; set; }
public string ErrorId { get; set; }
public string ErrorMessage { get; set; }
}
}
}

View File

@@ -1,9 +1,3 @@
// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
using System.Threading.Tasks;
using IdentityServer4.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Hosting;
@@ -11,17 +5,14 @@ using Microsoft.Extensions.Logging;
namespace OpenAuth.IdentityServer.Quickstart.Home
{
[SecurityHeaders]
[AllowAnonymous]
public class HomeController : Controller
{
private readonly IIdentityServerInteractionService _interaction;
private readonly IHostEnvironment _environment;
private readonly ILogger _logger;
public HomeController(IIdentityServerInteractionService interaction, IHostEnvironment environment, ILogger<HomeController> logger)
public HomeController(IHostEnvironment environment, ILogger<HomeController> logger)
{
_interaction = interaction;
_environment = environment;
_logger = logger;
}
@@ -30,7 +21,6 @@ namespace OpenAuth.IdentityServer.Quickstart.Home
{
if (_environment.IsDevelopment())
{
// only show in development
return View();
}
@@ -39,24 +29,14 @@ namespace OpenAuth.IdentityServer.Quickstart.Home
}
/// <summary>
/// Shows the error page
/// 显示错误页面
/// </summary>
public async Task<IActionResult> Error(string errorId)
public IActionResult Error(string errorId)
{
var vm = new ErrorViewModel();
// retrieve error details from identityserver
var message = await _interaction.GetErrorContextAsync(errorId);
if (message != null)
var vm = new ErrorViewModel
{
vm.Error = message;
if (!_environment.IsDevelopment())
{
// only show in development
message.ErrorDescription = null;
}
}
ErrorId = errorId
};
return View("Error", vm);
}

View File

@@ -1,27 +0,0 @@
// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
using System.Collections.Generic;
using System.Security.Claims;
using IdentityModel;
using IdentityServer4.Test;
using Infrastructure;
using OpenAuth.App;
namespace OpenAuth.IdentityServer.Quickstart
{
public class TestUsers
{
public static List<TestUser> Users = new List<TestUser>
{
new TestUser{SubjectId = "System", Username = Define.SYSTEM_USERNAME, Password = Define.SYSTEM_USERPWD,
Claims =
{
new Claim(JwtClaimTypes.Name, "System"),
new Claim(JwtClaimTypes.GivenName, "yubao"),
new Claim(JwtClaimTypes.FamilyName, "lee")}
}
};
}
}

View File

@@ -1,11 +1,8 @@
// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
using System;
using System;
using System.Linq;
using Autofac;
using Infrastructure;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.EntityFrameworkCore;
@@ -14,7 +11,12 @@ using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using OpenAuth.App;
using OpenAuth.Repository;
using OpenIddict.Abstractions;
using SqlSugar;
using OpenIddict.Server;
using OpenAuth.IdentityServer.Handlers;
using static OpenIddict.Abstractions.OpenIddictConstants;
using static OpenIddict.Server.OpenIddictServerEvents;
namespace OpenAuth.IdentityServer
{
@@ -32,41 +34,69 @@ namespace OpenAuth.IdentityServer
{
services.AddControllersWithViews();
var builder = services.AddIdentityServer()
.AddInMemoryIdentityResources(Config.GetIdentityResources())
.AddInMemoryApiResources(Config.GetApis())
.AddInMemoryClients(Config.GetClients(Environment.IsProduction()))
.AddProfileService<CustomProfileService>();
// Cookie 认证(用于登录页面维持会话)
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options =>
{
options.LoginPath = "/Account/Login";
options.LogoutPath = "/Account/Logout";
});
// OpenIddict 配置
services.AddOpenIddict()
.AddServer(options =>
{
// 启用授权码流程 + PKCEVue3 前端使用)
options.AllowAuthorizationCodeFlow()
.RequireProofKeyForCodeExchange();
// 启用隐式流程Swagger 使用)
options.AllowImplicitFlow();
// 配置端点 URI
options.SetAuthorizationEndpointUris("/connect/authorize")
.SetTokenEndpointUris("/connect/token")
.SetUserinfoEndpointUris("/connect/userinfo")
.SetLogoutEndpointUris("/connect/logout");
// 注册 scope
options.RegisterScopes(Scopes.OpenId, Scopes.Profile, "openauthapi");
// 开发环境使用临时证书
options.AddDevelopmentEncryptionCertificate()
.AddDevelopmentSigningCertificate();
// 禁用 access_token 加密,让 WebApi 的 JwtBearer 能直接验证
options.DisableAccessTokenEncryption();
// 禁用降级模式下的强制 scope/client 验证
options.UseAspNetCore()
.EnableAuthorizationEndpointPassthrough()
.EnableTokenEndpointPassthrough()
.EnableUserinfoEndpointPassthrough()
.EnableLogoutEndpointPassthrough()
.DisableTransportSecurityRequirement(); // 开发环境允许 HTTP生产环境应移除
// 降级模式:不使用数据库存储,手动处理验证
options.EnableDegradedMode();
// 注册自定义验证 handler降级模式必须
options.AddEventHandler<ValidateAuthorizationRequestContext>(builder =>
builder.UseSingletonHandler<ValidateAuthorizationRequestHandler>());
options.AddEventHandler<ValidateTokenRequestContext>(builder =>
builder.UseSingletonHandler<ValidateTokenRequestHandler>());
options.AddEventHandler<ValidateLogoutRequestContext>(builder =>
builder.UseSingletonHandler<ValidateLogoutRequestHandler>());
})
.AddValidation(options =>
{
options.UseLocalServer();
options.UseAspNetCore();
});
services.ConfigureNonBreakingSameSiteCookies();
services.AddCors();
// todo:如果正式 环境请用下面的方式限制随意访问跨域
// var origins = new []
// {
// "http://localhost:1803",
// "http://localhost:52789"
// };
// if (Environment.IsProduction())
// {
// origins = new []
// {
// "http://demo.openauth.net.cn:1803",
// "http://demo.openauth.net.cn:52789"
// };
// }
// services.AddCors(option=>option.AddPolicy("cors", policy =>
// policy.AllowAnyHeader().AllowAnyMethod().AllowCredentials().WithOrigins(origins)));
//全部用测试环境正式环境请参考https://www.cnblogs.com/guolianyu/p/9872661.html
//if (Environment.IsDevelopment())
//{
builder.AddDeveloperSigningCredential();
//}
//else
//{
// throw new Exception("need to configure key material");
//}
services.AddAuthentication();
@@ -153,7 +183,7 @@ namespace OpenAuth.IdentityServer
app.UseStaticFiles();
app.UseRouting();
app.UseIdentityServer();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>

View File

@@ -1,82 +0,0 @@
@model OpenAuth.IdentityServer.Quickstart.Consent.ConsentViewModel
<div class="page-consent">
<div class="row page-header">
<div class="col-sm-10">
@if (Model.ClientLogoUrl != null)
{
<div class="client-logo"><img src="@Model.ClientLogoUrl"></div>
}
<h1>
@Model.ClientName
<small>is requesting your permission</small>
</h1>
</div>
</div>
<div class="row">
<div class="col-sm-8">
<partial name="_ValidationSummary" />
<form asp-action="Index" class="consent-form">
<input type="hidden" asp-for="ReturnUrl" />
<div>Uncheck the permissions you do not wish to grant.</div>
@if (Model.IdentityScopes.Any())
{
<div class="panel panel-default consent-buttons">
<div class="panel-heading">
<span class="glyphicon glyphicon-user"></span>
Personal Information
</div>
<ul class="list-group">
@foreach (var scope in Model.IdentityScopes)
{
<partial name="_ScopeListItem" model="@scope" />
}
</ul>
</div>
}
@if (Model.ResourceScopes.Any())
{
<div class="panel panel-default">
<div class="panel-heading">
<span class="glyphicon glyphicon-tasks"></span>
Application Access
</div>
<ul class="list-group">
@foreach (var scope in Model.ResourceScopes)
{
<partial name="_ScopeListItem" model="scope" />
}
</ul>
</div>
}
@if (Model.AllowRememberConsent)
{
<div class="consent-remember">
<label>
<input class="consent-scopecheck" asp-for="RememberConsent" />
<strong>Remember My Decision</strong>
</label>
</div>
}
<div class="consent-buttons">
<button name="button" value="yes" class="btn btn-primary" autofocus>Yes, Allow</button>
<button name="button" value="no" class="btn">No, Do Not Allow</button>
@if (Model.ClientUrl != null)
{
<a class="pull-right btn btn-default" target="_blank" href="@Model.ClientUrl">
<span class="glyphicon glyphicon-info-sign"></span>
<strong>@Model.ClientName</strong>
</a>
}
</div>
</form>
</div>
</div>
</div>

View File

@@ -1,6 +0,0 @@
<div class="page-header">
<h1>
Success
<small>You have successfully authorized the device</small>
</h1>
</div>

View File

@@ -1,14 +0,0 @@
@model string
<div class="page-header">
<h1>
User Code
</h1>
<p>
Please enter the code displayed on your device
</p>
<form asp-action="UserCodeCapture" method="post">
<input for="userCode" name="userCode" />
<button class="btn btn-primary">Submit</button>
</form>
</div>

View File

@@ -1,93 +0,0 @@
@model OpenAuth.IdentityServer.Quickstart.Device.DeviceAuthorizationViewModel
<div class="page-header">
<div class="row page-header">
<div class="col-sm-10">
@if (Model.ClientLogoUrl != null)
{
<div class="client-logo"><img src="@Model.ClientLogoUrl"></div>
}
<h1>
@Model.ClientName
<small>is requesting your permission</small>
</h1>
</div>
</div>
@if (Model.ConfirmUserCode)
{
<div class="row">
<div class="col-sm-8">
<p>
Please confirm that the authorization request quotes the code: "@Model.UserCode".
</p>
</div>
</div>
}
<div class="row">
<div class="col-sm-8">
<partial name="_ValidationSummary" />
<form asp-action="Callback" class="consent-form">
<input asp-for="UserCode" type="hidden" value="@Model.UserCode" />
<div>Uncheck the permissions you do not wish to grant.</div>
@if (Model.IdentityScopes.Any())
{
<div class="panel panel-default consent-buttons">
<div class="panel-heading">
<span class="glyphicon glyphicon-user"></span>
Personal Information
</div>
<ul class="list-group">
@foreach (var scope in Model.IdentityScopes)
{
<partial name="_ScopeListItem" model="@scope" />
}
</ul>
</div>
}
@if (Model.ResourceScopes.Any())
{
<div class="panel panel-default">
<div class="panel-heading">
<span class="glyphicon glyphicon-tasks"></span>
Application Access
</div>
<ul class="list-group">
@foreach (var scope in Model.ResourceScopes)
{
<partial name="_ScopeListItem" model="scope" />
}
</ul>
</div>
}
@if (Model.AllowRememberConsent)
{
<div class="consent-remember">
<label>
<input class="consent-scopecheck" asp-for="RememberConsent" />
<strong>Remember My Decision</strong>
</label>
</div>
}
<div class="consent-buttons">
<button name="button" value="yes" class="btn btn-primary" autofocus>Yes, Allow</button>
<button name="button" value="no" class="btn">No, Do Not Allow</button>
@if (Model.ClientUrl != null)
{
<a class="pull-right btn btn-default" target="_blank" href="@Model.ClientUrl">
<span class="glyphicon glyphicon-info-sign"></span>
<strong>@Model.ClientName</strong>
</a>
}
</div>
</form>
</div>
</div>
</div>

View File

@@ -1,32 +0,0 @@
@model OpenAuth.IdentityServer.Quickstart.Diagnostics.DiagnosticsViewModel
<h1>Authentication cookie</h1>
<h3>Claims</h3>
<dl>
@foreach (var claim in Model.AuthenticateResult.Principal.Claims)
{
<dt>@claim.Type</dt>
<dd>@claim.Value</dd>
}
</dl>
<h3>Properties</h3>
<dl>
@foreach (var prop in Model.AuthenticateResult.Properties.Items)
{
<dt>@prop.Key</dt>
<dd>@prop.Value</dd>
}
</dl>
@if (Model.Clients.Any())
{
<h3>Clients</h3>
<ul>
@foreach (var client in Model.Clients)
{
<li>@client</li>
}
</ul>
}

View File

@@ -1,79 +0,0 @@
@model OpenAuth.IdentityServer.Quickstart.Grants.GrantsViewModel
<div class="grants">
<div class="row page-header">
<div class="col-sm-10">
<h1>
Client Application Access
</h1>
<div>Below is the list of applications you have given access to and the names of the resources they have access to.</div>
</div>
</div>
@if (Model.Grants.Any() == false)
{
<div class="row">
<div class="col-sm-8">
<div class="alert alert-info">
You have not given access to any applications
</div>
</div>
</div>
}
else
{
foreach (var grant in Model.Grants)
{
<div class="row grant">
<div class="col-sm-2">
@if (grant.ClientLogoUrl != null)
{
<img src="@grant.ClientLogoUrl">
}
</div>
<div class="col-sm-8">
<div class="clientname">@grant.ClientName</div>
<div>
<span class="created">Created:</span> @grant.Created.ToString("yyyy-MM-dd")
</div>
@if (grant.Expires.HasValue)
{
<div>
<span class="expires">Expires:</span> @grant.Expires.Value.ToString("yyyy-MM-dd")
</div>
}
@if (grant.IdentityGrantNames.Any())
{
<div>
<div class="granttype">Identity Grants</div>
<ul>
@foreach (var name in grant.IdentityGrantNames)
{
<li>@name</li>
}
</ul>
</div>
}
@if (grant.ApiGrantNames.Any())
{
<div>
<div class="granttype">API Grants</div>
<ul>
@foreach (var name in grant.ApiGrantNames)
{
<li>@name</li>
}
</ul>
</div>
}
</div>
<div class="col-sm-2">
<form asp-action="Revoke">
<input type="hidden" name="clientId" value="@grant.ClientId">
<button class="btn btn-danger">Revoke Access</button>
</form>
</div>
</div>
}
}
</div>

View File

@@ -1,14 +1,10 @@
@{
var version = typeof(IdentityServer4.Hosting.IdentityServerMiddleware).Assembly.GetName().Version.ToString();
}
<div class="welcome-page">
<div class="row page-header">
<div class="col-sm-10">
<h1>
<img class="icon" src="~/icon.jpg">
Welcome to IdentityServer4
<small>(version @version)</small>
Welcome to OpenAuth.Net IdentityServer
<small>(OpenIddict)</small>
</h1>
</div>
</div>
@@ -21,19 +17,5 @@
where you can find metadata and links to all the endpoints, key material, etc.
</p>
</div>
<div class="col-sm-8">
<p>
Click <a href="~/grants">here</a> to manage your stored grants.
</p>
</div>
</div>
<div class="row">
<div class="col-sm-8">
<p>
Here are links to the
<a href="https://github.com/identityserver/IdentityServer4">source code repository</a>,
and <a href="https://github.com/IdentityServer/IdentityServer4/tree/master/samples">ready to use samples</a>.
</p>
</div>
</div>
</div>

View File

@@ -1,11 +1,5 @@
@model OpenAuth.IdentityServer.Quickstart.Home.ErrorViewModel
@{
var error = Model?.Error?.Error;
var errorDescription = Model?.Error?.ErrorDescription;
var request_id = Model?.Error?.RequestId;
}
<div class="error-page">
<div class="page-header">
<h1>Error</h1>
@@ -16,25 +10,16 @@
<div class="alert alert-danger">
Sorry, there was an error
@if (error != null)
@if (!string.IsNullOrEmpty(Model?.ErrorId))
{
<strong>
<em>
: @error
</em>
</strong>
<div>Error Id: @Model.ErrorId</div>
}
if (errorDescription != null)
{
<div>@errorDescription</div>
}
@if (!string.IsNullOrEmpty(Model?.ErrorMessage))
{
<div>@Model.ErrorMessage</div>
}
</div>
@if (request_id != null)
{
<div class="request-id">Request Id: @request_id</div>
}
</div>
</div>
</div>

View File

@@ -1,9 +1,8 @@
@using IdentityServer4.Extensions
@{
@{
string name = null;
if (!true.Equals(ViewData["signed-out"]))
{
name = Context.User?.GetDisplayName();
name = Context.User?.Identity?.IsAuthenticated == true ? Context.User.Identity.Name : null;
}
}
<!DOCTYPE html>

View File

@@ -1,34 +0,0 @@
@model OpenAuth.IdentityServer.Quickstart.Consent.ScopeViewModel
<li class="list-group-item">
<label>
<input class="consent-scopecheck"
type="checkbox"
name="ScopesConsented"
id="scopes_@Model.Name"
value="@Model.Name"
checked="@Model.Checked"
disabled="@Model.Required" />
@if (Model.Required)
{
<input type="hidden"
name="ScopesConsented"
value="@Model.Name" />
}
<strong>@Model.DisplayName</strong>
@if (Model.Emphasize)
{
<span class="glyphicon glyphicon-exclamation-sign"></span>
}
</label>
@if (Model.Required)
{
<span><em>(required)</em></span>
}
@if (Model.Description != null)
{
<div class="consent-description">
<label for="scopes_@Model.Name">@Model.Description</label>
</div>
}
</li>

View File

@@ -30,7 +30,6 @@
<ItemGroup>
<PackageReference Include="log4net" Version="2.0.12" />
<PackageReference Include="IdentityServer4.AccessTokenValidation" Version="3.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="3.1.2" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="3.1.2" />
<PackageReference Include="Microsoft.AspNetCore.TestHost" Version="3.1.2" />

View File

@@ -4,7 +4,7 @@ using System.IO;
using System.Linq;
using System.Reflection;
using Autofac;
using IdentityServer4.AccessTokenValidation;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Infrastructure;
using Infrastructure.Extensions.AutofacManager;
using Infrastructure.Middleware;
@@ -52,7 +52,7 @@ namespace OpenAuth.WebApi
{
services.AddAuthorization();
services.AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme)
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.Authority = identityServer;