Мое решение:
- Создайте пользовательский код, который выдает код авторизации для app1 и перенаправляет его в app2.
- Создание реализации гранта расширения, которая аутентифицирует пользователя по коду.
Давайте сначала посмотрим, как это работает:
- Авторизовать пользователя (я использовал почтальона).
Запрос:
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» - это означает, что мы можем запросить код.
- Запросить перенаправление на WebApp2 с кодом и состоянием.
Запрос:
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
Обратите внимание, мы добавили токен с первого шага к этому запросу на получение и нас перенаправили на «http://WebApp2.test.url? Code = random_base64_value_generated_in_is4_api & state = random_base64_value_generated_in_spa1_at_the_begining » * 1038
- Итак, теперь пользователь может проходить аутентификацию из WebApp2 по коду и состоянию.
Запрос:
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 знает, кто (суб) инициировал перенаправление.
код ( раствор на 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";
}
}
}