Как реализовать IdentityServer4 SSO на основе потока паролей? - PullRequest
1 голос
/ 29 марта 2019

У меня есть два веб-приложения:

  1. webApp1 в домене1 (Spa1 -> WebApi1 -> IdentityServer4 -> db1)

  2. webApp2 вdomain2 (Spa2 -> WebApi2 -> db2)

История пользователя:

  1. Конечный пользователь Джон уже авторизован потоком паролей в IdentityServer4, расположенном в WebApi1, поэтомуУ Spa1 есть JWT с областью «WebApi1» и токен обновления.
  2. Джон в Spa1 нажимает кнопку «Перейти к Spa2», после чего он перенаправляется на Spa2.
  3. В браузере Джонас Spa2 открывается новая страница, и Джон видит, что он уже аутентифицирован IdentityServer4 в Spa2 и авторизован для WebApi2 (Джон может использовать функциональность Spa2 без дополнительного диалогового окна входа в систему), потому что db2 имеет отображение db1.users-> db2.users (такwebApp2 может использовать свои собственные роли).

Это похоже на сценарий, когда пользователь читает почту в спа-салоне gmail и из письма переходит по ссылке на YouTube (без дополнительных действий аутентификации) и видит, чтоон уже authenticated by google.

Я хотел использовать поток кода авторизации с config

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?

Ответы [ 2 ]

1 голос
/ 29 марта 2019

Это не работает для вас, потому что у вас есть потоки входа, реализованные с помощью ResourceOwnerCredentials типа предоставления, что означает, что когда пользователь John обращается к spa1, spa1 регистрирует пользователя John через пользовательский поток входа.

Для того, чтобы это работало «из коробки», самый простой способ и наиболее рекомендуемый подход, вероятно, заключался бы в преобразовании spa1 в один из предпочтительных типов грантов (например, Implicit или AuthorizationCode), а затем один раз для пользователя. Джон входит в систему через центральную страницу входа, обслуживаемую вашим IdentityServer 4, он оставляет куки-файлы, а затем любые последующие попытки запроса токена напрямую регистрируют пользователя и выдают запрошенные токены соответствующим клиентским приложениям (также можно при желании пропустить согласие, как вы это сделали). в вашем примере).

Есть еще один способ, который я могу придумать, который я бы не рекомендовал вам, но я лично реализовал его один раз из-за особых требований клиента, чтобы сохранить тип предоставления ResourceOwnerCredentials и настраиваемую страницу входа, но все же достичь единый знак поведения. Если оба, ваш spa1 и IdentityServer4, размещены в одном и том же домене (таким образом, spa1.yourdomain.com и auth.yourdomain.com) и ваше хранилище пользователей (имена пользователей и учетные данные) совместно используются между IdentityServer4 и spa1, вы можете технически, если пользователь введет учетные данные в своем Страница входа в spa1, программно отправьте запрос POST на страницу входа в систему сервера идентификации 4 с информацией о форме, содержащей учетные данные пользователя, получите файл cookie из ответа и затем сохраните файл cookie в клиенте пользователя. Всякий раз, когда ваш пользователь Джон будет пытаться получить доступ к spa2, перенаправление на IdentityServer4 все равно будет происходить, но весь поток входа в систему будет обойден, потому что уже будет файл cookie для автоматического входа пользователя. Если вы решите реализовать что-то в этом направлении Пожалуйста, не забудьте изучить проблемы безопасности (их будет много) и по-настоящему оценить, требуется ли это.

0 голосов
/ 05 мая 2019

Мое решение:

  1. Создайте пользовательский код, который выдает код авторизации для app1 и перенаправляет его в app2.
  2. Создание реализации гранта расширения, которая аутентифицирует пользователя по коду.

Давайте сначала посмотрим, как это работает:

  1. Авторизовать пользователя (я использовал почтальона).

Запрос:

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» - это означает, что мы можем запросить код.

  1. Запросить перенаправление на 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

  1. Итак, теперь пользователь может проходить аутентификацию из 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";
        }
    }
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...