我有两个网络应用程序:
webApp1 (Spa1-> WebApi1-> IdentityServer4-> db1)
webApp2 (Spa2-> WebApi2-> db2)
用户故事:
这类似于以下情况:用户在gmail spa中阅读了一封邮件,并从一封信中跟随了指向youtube的链接(没有其他身份验证操作) 并且发现他已经通过Google身份验证。
我想在配置中使用授权码流
new Client
{
ClientId = "app1,
AllowedGrantTypes = GrantTypes.ResourceOwnerPassword,
AccessTokenType = AccessTokenType.Jwt,
ClientSecrets =
{
new Secret("secret1".Sha256())
},
AllowedScopes = { "api1"},
AllowOfflineAccess = true,
AlwaysSendClientClaims = true,
AlwaysIncludeUserClaimsInIdToken = true
},
new Client {
ClientId = "app2",
ClientSecrets =
{
new Secret("secret2".Sha256())
},
Enabled = true,
AllowedGrantTypes = GrantTypes.Code,
RequireConsent = false,
AllowRememberConsent = false,
RedirectUris =
new List<string> {
"http://localhost:5436/account/oAuth2"
},
AllowedScopes = { "api2" },
AccessTokenType = AccessTokenType.Jwt
}
,但需要通过浏览器进行其他身份验证 这是不必要的过程,因为用户已通过身份验证。
我应该如何在IdentityServer4中实现此身份验证方案?
答案 0 :(得分:1)
这对您不起作用,因为您通过ResourceOwnerCredentials
授予类型实现了登录流程,这意味着当用户John访问spa1时,spa1通过自定义登录流程记录了用户John。
为使此工作立即可用,最简单的方法和最推荐的方法可能是将spa1转换为使用首选授予类型之一(例如,Implicit
或AuthorizationCode
)然后,一旦用户John通过您的IdentityServer 4服务的中央登录页面登录,它将留下cookie,然后任何后续令牌请求尝试都将直接登录用户,并将请求的令牌发布到适当的客户端应用程序(也可以跳过同意)就像您在示例中所做的一样。
我可以考虑另一种方法,我不建议您这样做,但是由于特定的客户要求,我保留了ResourceOwnerCredentials
授予类型和自定义登录页面,因此我亲自实施了该方法,但是仍然实现单点登录行为。如果您的spa1和IdentityServer4都托管在同一个域(因此spa1.yourdomain.com
和auth.yourdomain.com
)上,并且您的用户存储(用户名和凭据)在IdentityServer4和spa1之间共享,那么您可以从技术上对用户进行授权在spa1登录页面中输入凭据,以编程方式将POST
请求提交给身份服务器4登录页面,其中包含包含用户凭据的表单详细信息,从响应中获取cookie,然后将cookie存储在用户的客户端中。每当您的用户John尝试访问spa2时,仍然会发生重定向到IdentityServer4的操作,但是会规避整个登录流程,因为已经有一个cookie可以自动使用户登录。如果您决定按照以下方式实施某些操作,请确保研究安全问题(会有很多问题)并真正评估是否需要这样做。
答案 1 :(得分:0)
我的解决方案:
首先让我们看看它是如何工作的:
请求:
POST /connect/token
Content-Type: application/x-www-form-urlencoded
Host: localhost:5000
grant_type=password&client_id=app1&client_secret=app1secret&scope=offline_access%20app1.api%20auth.api&username=tu1&password=111111
响应:
{"access_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6IjE0MDExYjViNGM0ZGYxYTUzZWFhMzhiMjBiZWVlOGM5IiwidHlwIjoiSldUIn0.eyJuYmYiOjE1NTcwODE1OTYsImV4cCI6MTU1NzA4NTE5NiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo1MDAwIiwiYXVkIjpbImh0dHA6Ly9sb2NhbGhvc3Q6NTAwMC9yZXNvdXJjZXMiLCJhcHAxLmFwaSIsImF1dGguYXBpIl0sImNsaWVudF9pZCI6ImFwcDEiLCJzdWIiOiJ0dTEiLCJhdXRoX3RpbWUiOjE1NTcwODE1OTYsImlkcCI6ImxvY2FsIiwic2NvcGUiOlsiYXBwMS5hcGkiLCJhdXRoLmFwaSIsIm9mZmxpbmVfYWNjZXNzIl0sImFtciI6WyJwd2QiXX0.bV1lvPs8AFUq7kcCAEMz4rS2vOmUIzrogN3EByQViBkKNFF6ijrizVc2GxiXRNTwl35Kgsb7beoFaVy4Ai2RmyMxmyJumwiwR0-wbX_mrs-XcfADfhEdLQJWLvAkbm2jm3FvDC-7F6S5Mip-QtbcXdgqg5oQo53nBJDXc7bsn1MaKPkivR1tg9CjA0uOQC891aBr4BzRZeH43YpVjxO7zzYL9vcplIL79nkhiG4iVfo7Ti8JJa4Q7HzH6lj0V_NrTY3BRzvCHVPNy0cFtfFTE1l_abMel1ftozyvFtrsTgVqRZhFfzY0d_7K8M9wtXAa7vbYW7oAhvnxVlga4HX_zg",
"expires_in": 3600,
"token_type": "Bearer",
"refresh_token": "26df6326251b7590cf6eb9898967e814ff291712aa7504ac84f9d8ae07374d3c"}
好!我们获得了带有有效负载的令牌:
{
"nbf": 1557072351,
"exp": 1557075951,
"iss": "http://localhost:5000",
"aud": [
"http://localhost:5000/resources",
"app1.api",
"auth.api"
],
"client_id": "app1",
"sub": "tu1",
"auth_time": 1557072351,
"idp": "local",
"scope": [
"app1.api",
"auth.api", //!!!
"offline_access"
],
"amr": [
"pwd"
]
}
令牌的范围为“ auth.api”-这意味着我们可以请求代码。
请求:
GET /api/CodeAuthority?state=random_base64_value_generated_in_spa1_at_the_begining
Authorization: Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6IjE0MDExYjViNGM0ZGYxYTUzZWFhMzhiMjBiZWVlOGM5IiwidHlwIjoiSldUIn0.eyJuYmYiOjE1NTcwODE1OTYsImV4cCI6MTU1NzA4NTE5NiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo1MDAwIiwiYXVkIjpbImh0dHA6Ly9sb2NhbGhvc3Q6NTAwMC9yZXNvdXJjZXMiLCJhcHAxLmFwaSIsImF1dGguYXBpIl0sImNsaWVudF9pZCI6ImFwcDEiLCJzdWIiOiJ0dTEiLCJhdXRoX3RpbWUiOjE1NTcwODE1OTYsImlkcCI6ImxvY2FsIiwic2NvcGUiOlsiYXBwMS5hcGkiLCJhdXRoLmFwaSIsIm9mZmxpbmVfYWNjZXNzIl0sImFtciI6WyJwd2QiXX0.bV1lvPs8AFUq7kcCAEMz4rS2vOmUIzrogN3EByQViBkKNFF6ijrizVc2GxiXRNTwl35Kgsb7beoFaVy4Ai2RmyMxmyJumwiwR0-wbX_mrs-XcfADfhEdLQJWLvAkbm2jm3FvDC-7F6S5Mip-QtbcXdgqg5oQo53nBJDXc7bsn1MaKPkivR1tg9CjA0uOQC891aBr4BzRZeH43YpVjxO7zzYL9vcplIL79nkhiG4iVfo7Ti8JJa4Q7HzH6lj0V_NrTY3BRzvCHVPNy0cFtfFTE1l_abMel1ftozyvFtrsTgVqRZhFfzY0d_7K8M9wtXAa7vbYW7oAhvnxVlga4HX_zg
User-Agent: PostmanRuntime/7.11.0
Accept: */*
Cache-Control: no-cache
Postman-Token: 8bec320a-0cc9-4aeb-aba1-acdbd89384cf
Host: localhost:5000
accept-encoding: gzip, deflate
Connection: keep-alive
响应:
HTTP/1.1 302
status: 302
Date: Sun, 05 May 2019 19:16:44 GMT
Server: Kestrel
Content-Length: 0
Location: http://WebApp2.test.url?code=random_base64_value_generated_in_is4_api&state=random_base64_value_generated_in_spa1_at_the_begining
请注意,我们将令牌从第一步放置到此get请求中,我们已被重定向到“ http://WebApp2.test.url?code=random_base64_value_generated_in_is4_api&state=random_base64_value_generated_in_spa1_at_the_begining”。
请求:
POST /connect/token
Content-Type: application/x-www-form-urlencoded
User-Agent: PostmanRuntime/7.11.0
Accept: */*
Cache-Control: no-cache
Postman-Token: 4adc90e8-ae6a-421b-8514-8b96e0f7108a
Host: localhost:5000
accept-encoding: gzip, deflate
content-length: 197
Connection: keep-alive
grant_type=app2_auth_code&code=random_base64_value_generated_in_is4_api&client_id=app2&client_secret=app2secret&scope=code.authentication&state=random_base64_value_generated_in_spa1_at_the_begining
响应:
HTTP/1.1 200
status: 200
Date: Sun, 05 May 2019 19:25:41 GMT
Content-Type: application/json; charset=UTF-8
Server: Kestrel
Cache-Control: no-store, no-cache, max-age=0
Pragma: no-cache
Transfer-Encoding: chunked
{"access_token":"eyJhbGciOiJSUzI1NiIsImtpZCI6IjE0MDExYjViNGM0ZGYxYTUzZWFhMzhiMjBiZWVlOGM5IiwidHlwIjoiSldUIn0.eyJuYmYiOjE1NTcwODQzNDEsImV4cCI6MTU1NzA4Nzk0MSwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo1MDAwIiwiYXVkIjpbImh0dHA6Ly9sb2NhbGhvc3Q6NTAwMC9yZXNvdXJjZXMiLCJjb2RlLmF1dGhlbnRpY2F0aW9uIl0sImNsaWVudF9pZCI6ImFwcDIiLCJzdWIiOiJ0dTEiLCJhdXRoX3RpbWUiOjE1NTcwODQzNDEsImlkcCI6ImxvY2FsIiwic2NvcGUiOlsiY29kZS5hdXRoZW50aWNhdGlvbiJdLCJhbXIiOlsiYXBwMl9hdXRoX2NvZGUiXX0.bDonw4SjGGqgwxnJeJoBP4-DfjWcAXUsXrvBx5Qav3cS329g9qciXzBcEpFmNB41De3GW-ocVFb8AFgGGCTENW3B2lL9HdopJ9C2ksPRwB1qTJ9S98HZZjOT0wQ2N-AbfQWAJlH12qGeml2UjB-L-afFAPVM-KpOh4my9znvUJWV_L_7q2Lwpv23fSkyGDahQCcZVLcurCjx8uQp1xliOF7b6qZ87kwh5brxGvUXP3oWjfmBvG_PsAFvGHZwgicjTWK7ED_OGTULCvtCtNO5RwW9_HINIl-217KnYgsrHNfaFCiv03vKXckvmkzfacreO0FaDr3r0nS2dMGrkyZ2sA","expires_in":3600,"token_type":"Bearer"}
我们只是交换了令牌的代码和状态:
{
"nbf": 1557073472,
"exp": 1557077072,
"iss": "http://localhost:5000",
"aud": [
"http://localhost:5000/resources",
"code.authentication"
],
"client_id": "app2",
"sub": "tu1", //!!!
"auth_time": 1557073472,
"idp": "local",
"scope": [
"code.authentication" //!!!
],
"amr": [
"app2_auth_code"
]
}
现在,WebApp2知道谁(子)发起了重定向。
代码(Solution on github):
IdentityServer4:
namespace TestIdentityServer4
{
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddIdentityServer()
.AddDeveloperSigningCredential()
.AddInMemoryApiResources(new List<ApiResource>()
{
//Api which returns redirect url with code and state.
new ApiResource("auth.api", "Auth API"),
//App1 api. Just to show that app1 has some functionality (IdentityController).
new ApiResource("app1.api", "App1 API"),
//This resource is authentification functionality implemented by AuthCodeValidator.
new ApiResource("code.authentication", "Authentication by code")
})
.AddInMemoryClients(new List<Client>()
{
//web app1
new Client
{
ClientId = "app1",
ClientSecrets =
{
new Secret("app1secret".Sha256())
},
AllowedGrantTypes = GrantTypes.ResourceOwnerPassword,
AllowedScopes =
{
"app1.api",
"auth.api"
},
AllowOfflineAccess = true
},
//web app2
new Client
{
ClientId = "app2",
ClientSecrets = new List<Secret>
{
new Secret("app2secret".Sha256())
},
AllowedGrantTypes = { "app2_auth_code" },
AllowedScopes = new List<string>
{
"code.authentication"
}
}
})
//App1 users for test purpose
.AddTestUsers( new List<TestUser>()
{
new TestUser()
{
Username = "tu1",
Password = "111111",
SubjectId = "tu1"
}
})
//Regestring of the custom validator
.AddExtensionGrantValidator<AuthCodeValidator>();
//Our IS4 has the custom api (CodeAuthorityController). It is also a resorce that should be protected.
//It should be awailable fore user authorized in app1.
services.AddAuthentication(opt =>
{
opt.DefaultScheme = IdentityServerAuthenticationDefaults.AuthenticationScheme;
opt.DefaultAuthenticateScheme = IdentityServerAuthenticationDefaults.AuthenticationScheme;
})
.AddIdentityServerAuthentication(
opt =>
{
opt.Authority = "http://localhost:5000";
opt.RequireHttpsMetadata = false;
opt.ApiName = "auth.api";
});
services.AddMvc();
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseCors();
app.UseIdentityServer();
app.UseMvc();
}
}
}
代码授权:
namespace TestIdentityServer4.Controllers
{
[Route("api/[controller]")]
[ApiController]
[Authorize]
public class CodeAuthorityController : ControllerBase
{
[HttpGet()]
public IActionResult Get()
{
try
{
string state = this.Request.Query["state"];
if (string.IsNullOrEmpty(state))
return StatusCode(500);
var code = GenerateCode();
SaveCodeAndState(code, state);
return Redirect($"http://WebApp2.test.url?code={code}&state={state}");
}
catch (Exception e)
{
//Log e
return StatusCode(500);
}
}
private string GenerateCode()
{
//CryptoRandom.CreateUniqueId(16)
return "random_base64_value_generated_in_is4_api";
}
/// <summary>
/// Save the code hash and state hash to storage
/// </summary>
private void SaveCodeAndState(string code, string state)
{
//Save the code request ({requestId, app1SessionId, hash(code), hash(state), expTime}) to storage with exp time
//db.SaveCodeRequest(code.Sha256(), state.Sha256())
}
}
}
代码验证器:
namespace TestIdentityServer4.Validators
{
public class AuthCodeValidator : IExtensionGrantValidator
{
public string GrantType => "app2_auth_code";
public async Task ValidateAsync(ExtensionGrantValidationContext context)
{
var code = context.Request.Raw.Get("code");
var state = context.Request.Raw.Get("state");
var sub = GetSubByCode(code, state);
if (string.IsNullOrEmpty(sub))
{
context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant);
return;
}
context.Result = new GrantValidationResult(sub, GrantType);
return;
}
//Check the code and the state (and the request are still active) and returns sub
private string GetSubByCode(string code, string state)
{
return "tu1";
}
}
}