如何基于密码流实现IdentityServer4 SSO?

时间:2019-03-29 10:49:05

标签: c# authentication oauth-2.0 single-sign-on identityserver4

我有两个网络应用程序:

    domain1中的
  1. webApp1 (Spa1-> WebApi1-> IdentityServer4-> db1)

  2. domain2中的
  3. webApp2 (Spa2-> WebApi2-> db2)

用户故事:

  1. 最终用户John已经由WebApi1下的IdentityServer4中的密码流授权,因此Spa1的JWT具有“ WebApi1”作用域并刷新 令牌。
  2. Spa1中的John单击“转到Spa2”按钮,然后将他重新转到Spa2。
  3. 在John的浏览器中,Spa2打开了一个新页面,John看到他已经通过Spa2中的IdentityServer4进行了身份验证 并已获得WebApi2的授权(John可以使用Spa2功能而无需其他登录对话框),因为db2具有db1.users-> db2.users映射(因此webApp2可以使用其自己的角色)。

这类似于以下情况:用户在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中实现此身份验证方案?

2 个答案:

答案 0 :(得分:1)

这对您不起作用,因为您通过ResourceOwnerCredentials授予类型实现了登录流程,这意味着当用户John访问spa1时,spa1通过自定义登录流程记录了用户John。

为使此工作立即可用,最简单的方法和最推荐的方法可能是将spa1转换为使用首选授予类型之一(例如,ImplicitAuthorizationCode)然后,一旦用户John通过您的IdentityServer 4服务的中央登录页面登录,它将留下cookie,然后任何后续令牌请求尝试都将直接登录用户,并将请求的令牌发布到适当的客户端应用程序(也可以跳过同意)就像您在示例中所做的一样。

我可以考虑另一种方法,我不建议您这样做,但是由于特定的客户要求,我保留了ResourceOwnerCredentials授予类型和自定义登录页面,因此我亲自实施了该方法,但是仍然实现单点登录行为。如果您的spa1和IdentityServer4都托管在同一个域(因此spa1.yourdomain.comauth.yourdomain.com)上,并且您的用户存储(用户名和凭据)在IdentityServer4和spa1之间共享,那么您可以从技术上对用户进行授权在spa1登录页面中输入凭据,以编程方式将POST请求提交给身份服务器4登录页面,其中包含包含用户凭据的表单详细信息,从响应中获取cookie,然后将cookie存储在用户的客户端中。每当您的用户John尝试访问spa2时,仍然会发生重定向到IdentityServer4的操作,但是会规避整个登录流程,因为已经有一个cookie可以自动使用户登录。如果您决定按照以下方式实施某些操作,请确保研究安全问题(会有很多问题)并真正评估是否需要这样做。

答案 1 :(得分:0)

我的解决方案:

  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

请注意,我们将令牌从第一步放置到此get请求中,我们已被重定向到“ http://WebApp2.test.url?code=random_base64_value_generated_in_is4_api&state=random_base64_value_generated_in_spa1_at_the_begining”。

  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知道谁(子)发起了重定向。

代码(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";
        }
    }
}