【问题标题】:Return JWT Token generated by OAuthAuthorizatioServer from controller in Web API从 Web API 中的控制器返回由 OAuthAuthorizatioServer 生成的 JWT 令牌
【发布时间】:2016-08-20 05:54:51
【问题描述】:

在@Taiseer Joudeh 之后,我能够创建 Web API 的简单 POC。当我将 JWT 令牌添加到标头时,我可以创建新帐户,然后登录并调用安全 Web API。

我想修改负责创建帐户的方法。
现在我正在返回带有新用户对象的 Create (201) 代码,但我想返回访问令牌。

我找到了similar question,但它需要创建HttpClient 并向OAuthAuthorizatioServer TokenEndpointPath 发出请求。

Second question 我发现需要生成返回给前端的临时令牌,但是前端必须向服务器发出额外的请求以获取“真实”令牌。

我想做的是在创建用户帐户时返回登录响应(access_token、token_type 和 expires_in)。 我希望用户在创建帐户时进行身份验证。

我只使用了 Web API 和 JWT,没有任何 cookie。

编辑:我的临时解决方案:
创建用户后,我正在这样做:

var validTime = new TimeSpan(0, 0, 0, 10);
var identity = await UserManager.CreateIdentityAsync(user, "JWT");
var jwtFormat = new CustomJwtFormat(ApplicationConfiguration.Issuer);
var authenticationProperties = new AuthenticationProperties { IssuedUtc = DateTimeOffset.UtcNow, ExpiresUtc = DateTimeOffset.UtcNow.Add(validTime) };
var authenticationTicket = new AuthenticationTicket(identity, authenticationProperties);
var token = jwtFormat.Protect(authenticationTicket);

var response = new
{
    access_token = token,
    token_type = "bearer",
    expires_in = validTime.TotalSeconds.ToInt()
};

return Ok(response);

其中CustomJwtFormat 来自this awesome article

【问题讨论】:

    标签: c# asp.net asp.net-identity .net-4.5


    【解决方案1】:

    我正在使用确切的技术堆栈,并且最近成功实施了基于令牌的授权。我参考的链接非常巧妙地定义了 Web API 中基于令牌的身份验证。我必须说的必须书签页面。这是链接:TOKEN BASED AUTHENTICATION IN WEB APIs

    【讨论】:

    • 感谢链接,但我实际上使用 ASP.NET 身份和 JWT 作为令牌格式。我想避免重新发明轮子,尤其是出于安全考虑。正如您所写,这是一个必须拥有的书签,但对于想要手动创建令牌的人来说。
    【解决方案2】:

    在创建用户时发送带有 access_token、token_type 和 expires_in 的响应的想法是个好主意。但是,我会坚持使用 HttpClient 调用“/token”端点来完成这项任务。在生成令牌之前需要执行很多安全检查。由于这是安全性,我不会冒任何风险。我对使用行业专家提供的库/代码感到自在。

    也就是说,我试图提出一个类,您可以在创建用户后调用该类来创建令牌。此代码取自 Microsoft 在其 Katana 项目中实施的 OAuth 授权服务器。您可以访问源here。如您所见,在创建令牌时发生了很多事情。

    这是用于生成令牌的中间件类的修改版本。您必须提供正确的 OAuthAuthorizationServerOptions、上下文、用户名、密码、范围和客户端 ID 才能获得访问令牌。请注意,这是一个示例实现,可指导您朝着正确的方向前进。如果您想使用它,请彻底测试它。

    using System;
    using System.Collections.Generic;
    using System.Threading.Tasks;
    using AspNetIdentity.WebApi.Providers;
    using Microsoft.Owin;
    using Microsoft.Owin.Security;
    using Microsoft.Owin.Security.Infrastructure;
    using Microsoft.Owin.Security.OAuth;
    
    namespace WebApi.AccessToken
    {
        public class TokenGenerator
        {
            public string ClientId { get; set; }
            public string UserName { get; set; }
            public string Password { get; set; }
            public IList<string> Scope { get; set; }
    
            private OAuthAuthorizationServerOptions Options { get; } =
                new OAuthAuthorizationServerOptions()
                {
                    //For Dev enviroment only (on production should be AllowInsecureHttp = false)
                    AllowInsecureHttp = true,
                    TokenEndpointPath = new PathString("/oauth/token"),
                    AccessTokenExpireTimeSpan = TimeSpan.FromDays(1),
                    Provider = new CustomOAuthProvider(),
                    AccessTokenFormat = new CustomJwtFormat("http://localhost:59822")
                };
    
            public async Task<IList<KeyValuePair<string, string>>>  InvokeTokenEndpointAsync(IOwinContext owinContext)
            {
                var result = new List<KeyValuePair<string, string>>();
    
                DateTimeOffset currentUtc = Options.SystemClock.UtcNow;
                // remove milliseconds in case they don't round-trip
                currentUtc = currentUtc.Subtract(TimeSpan.FromMilliseconds(currentUtc.Millisecond));
    
                AuthenticationTicket ticket = await InvokeTokenEndpointResourceOwnerPasswordCredentialsGrantAsync(owinContext, Options, currentUtc);
    
                if (ticket == null)
                {
                    result.Add(new KeyValuePair<string, string>("ERROR", "Failed to create acess_token"));
                    return result;
                }
    
                ticket.Properties.IssuedUtc = currentUtc;
                ticket.Properties.ExpiresUtc = currentUtc.Add(Options.AccessTokenExpireTimeSpan);
                ticket = new AuthenticationTicket(ticket.Identity, ticket.Properties);
    
                var accessTokenContext = new AuthenticationTokenCreateContext(
                    owinContext,
                    Options.AccessTokenFormat,
                    ticket);
    
                await Options.AccessTokenProvider.CreateAsync(accessTokenContext);
                string accessToken = accessTokenContext.Token;
    
                if (string.IsNullOrEmpty(accessToken))
                {
                    accessToken = accessTokenContext.SerializeTicket();
                }
    
                DateTimeOffset? accessTokenExpiresUtc = ticket.Properties.ExpiresUtc;
    
                result.Add(new KeyValuePair<string, string>("access_token", accessToken));
                result.Add(new KeyValuePair<string, string>("token_type", "bearer"));
                TimeSpan? expiresTimeSpan = accessTokenExpiresUtc - currentUtc;
                var expiresIn = (long)expiresTimeSpan.Value.TotalSeconds;
                if (expiresIn > 0)
                {
                    result.Add(new KeyValuePair<string, string>("expires_in", "bearer"));
                }
                return result;
            }
    
            private async Task<AuthenticationTicket> InvokeTokenEndpointResourceOwnerPasswordCredentialsGrantAsync(IOwinContext owinContext, OAuthAuthorizationServerOptions options, DateTimeOffset currentUtc)
            {
                var grantContext = new OAuthGrantResourceOwnerCredentialsContext(
                    owinContext, 
                    options, 
                    ClientId, 
                    UserName, 
                    Password, 
                    Scope);
                await options.Provider.GrantResourceOwnerCredentials(grantContext);
                return grantContext.Ticket;
            }
        }
    }
    

    如果您有任何问题,请告诉我。

    谢谢你, 索玛。

    【讨论】:

    • 感谢您分享您的代码和链接。我注意到在我的代码中我有基于ISecureDataFormat 的自定义AccessTokenFormat 类。用于OAuthAuthorizationServer生成token。也许我可以使用它而不是创建新类。您能否添加一些代码来展示如何使用您的代码?
    • 请看看我更新的问题。我在项目中添加了一些代码作为临时解决方案。它有效,但也许可以改进。
    • Misiu,您所做的是在 asp.net 中的 OwinAuthenticationMiddleware 中实际发生的用于创建令牌的操作。在这里,您手动设置值。您引用的文章是一个很好的来源。如果您使用内置中间件生成用户登录的令牌(在创建之后),请谨慎使用,因为这两个可能不同。只要声明相同,就应该没问题。谢谢。
    【解决方案3】:

    下面是一些类似于我在使用 Asp.Net Core 1.0 的应用程序中所做的代码。如果您不使用 Core 1.0,您的登录和用户注册会有所不同。

    public async Task<string> CreateUser(string username, string password)
        {
            string jwt = String.Empty;
    
            if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(password))
            {
                Response.StatusCode = (int)HttpStatusCode.BadRequest;
            }
    
            var user = await _userManager.FindByNameAsync(username);
            if (user == null) // user doesn't exist, create user
            {
                var newUser = await _userManager.CreateAsync(new ApplicationUser() { UserName = username }, password); 
                if (newUser.Succeeded) //user was successfully created, sign in user
                {
                    user = await _userManager.FindByNameAsync(username);
                    var signInResult = await _signInManager.PasswordSignInAsync(user, password, false, true);
                    if (signInResult.Succeeded) //user signed in, create a JWT
                    {
                        var tokenHandler = new JwtSecurityTokenHandler();
                        List<Claim> userClaims = new List<Claim>();
    
                        //add any claims to the userClaims collection that you want to be part of the JWT
                        //...
    
                        ClaimsIdentity identity = new ClaimsIdentity(new GenericIdentity(user.UserName, "TokenAuth"), userClaims);
                        DateTime expires = DateTime.Now.AddMinutes(30); //or whatever
    
                        var securityToken = tokenHandler.CreateToken(
                            issuer: _tokenOptions.Issuer,  //_tokenAuthOptions is a class that holds the issuer, audience, and RSA security key
                            audience: _tokenOptions.Audience,
                            subject: identity,
                            notBefore: DateTime.Now,
                            expires: expires,
                            signingCredentials: _tokenOptions.SigningCredentials
                            );
    
                        jwt = tokenHandler.WriteToken(securityToken);
                        Response.StatusCode = (int)HttpStatusCode.Created;
                        await _signInManager.SignOutAsync(); //sign the user out, which deletes the cookie that gets added if you are using Identity.  It's not needed as security is based on the JWT
                    }
                }
                //handle other cases...
            }
        }
    

    基本上,用户被创建,然后自动登录。然后我构建一个 JWT(添加你想要的任何声明)并在响应正文中返回它。在客户端(MVC 和 Angular JS)我将 JWT 从响应正文中取出并存储。然后在每个后续请求的 Authorization 标头中将其传递回服务器。所有服务器操作的授权策略都基于 JWT 提供的声明集。没有 cookie,服务器上没有状态。

    编辑:我想您甚至不需要在此过程中调用 signIn 方法,因为您只需创建用户、创建 JWT 并返回 JWT。当用户登录未来的请求时,您将使用登录、创建令牌、注销方法。

    【讨论】:

    • 抱歉回复晚了,感谢您分享您的代码。不幸的是,我没有使用 ASP.NET Core 1.0,但您的代码让我知道使用 JwtSecurityTokenHandler。我马上试试!
    • 请看看我更新的问题。我在项目中添加了一些代码作为临时解决方案。它有效,但也许可以改进。
    • 我觉得不错。您设置作为令牌一部分的属性/声明,创建令牌,将其返回给用户。
    【解决方案4】:

    在 OAuth 解决方案中,您作为开发人员不需要自己处理 cookie 设置。 cookie 处理由框架自动为您完成。

    另外,设置会话的唯一方法是 a.使用会话 cookie 或 b。使用无 cookie(在 url 中)方法。查看http://www.cloudidentity.com/blog/2015/02/19/introducing-adal-js-v1/ 以获取有关令牌验证和会话建立的更多详细信息(同时搜索术语cookie,您将知道它的全部用途)。

    如果您开始考虑根本不使用 cookie,您不仅需要弄清楚如何维护会话并在不使用 cookie 的情况下安全地执行此操作,而且还必须重新编写令牌刷新代码来为您检测和刷新令牌基于会话 cookie 的存在。 (即不是一个聪明的主意)

    【讨论】:

    • 我没有使用 Active Directory。我只有使用 OWIN 和 Asp.Net Identity 2 的 WebAPI 解决方案,我已经按照这个博客系列完成了 POC:bitoftech.net/2015/03/31/… 在正常帐户创建(使用 WebAPI)之后,用户必须对令牌端点进行另一个请求以获取令牌,然后是该令牌被添加到标头中的每个请求中。令牌存储在客户端的本地存储中。前端在 Angular 中完成。没有任何会话或 cookie。我的问题不是 cookie,而是如何从 WebAPI 返回 JWT 令牌。
    【解决方案5】:

    我假设您指的是以下文章:http://bitoftech.net/2015/02/16/implement-oauth-json-web-tokens-authentication-in-asp-net-web-api-and-identity-2/

    在这种情况下,对用户进行身份验证的一般方法是

    1. 对令牌端点进行 HTTP 调用
    2. 用户在呈现的 UI 中输入凭据
    3. IdP 验证凭据并颁发令牌
    4. 用户再次调用授权端点,OWIN 将验证此 (JWT) 令牌(在 ConfigureOAuthTokenConsumption 方法中配置),如果成功,将设置与令牌到期相同的用户会话。会话是使用会话 cookie 设置的。

    现在尝试了解,一般来说,需要整个身份验证过程,因为您的服务器不相信登录的用户是该人声称的用户。但是,在您的情况下,您(或您的服务器代码)刚刚创建了一个用户,并且您确定访问您网站的人就是您刚刚创建的用户。在这种情况下,您无需验证令牌即可为该用户创建会话。只需创建一个适合您用例的到期会话。

    下次用户必须登录并使用令牌向服务器证明他/她自己,但这次用户不需要证明他/她自己。

    注意:如果您坚决要求使用令牌登录您自己刚刚使用其凭据创建的用户,这里有几个问题。

    • 您负责存储(有权访问)用户凭据,在应用程序的整个生命周期中您可能不会使用这些凭据(在大多数情况下,您可能希望充当依赖方而不是 IdP) .
    • 即使你想这样做也不是小事。您必须代表用户在代码(服务器或客户端)中调用令牌端点,为他们输入他们的凭据,检索令牌,在您的站点上调用经过身份验证的端点并检索会话 cookie对用户隐藏所有这些,如果你讨厌自己,你可能会这样做:),但也不是很安全的做事方式,尤其是当你首先不厌其烦地实施 OAuth 时。李>

    此外,请查看支持隐式授权的 Windows Server 2016(此时为技术预览版 5),如果您可以稍等一下 RTM,则可能无需编写所有这些自定义代码。

    【讨论】:

    • 感谢您如此详细的回复。就我而言,我没有使用 cookie 或会话。我在一台机器上有 WebAPI 服务器,调用是通过 javascript 从网站完成的。在我的情况下,我在用户注册时验证用户(他需要邀请码来创建帐户),然后我希望他能够例如填写其他字段而无需登录。在 Visual Studio(SPA 应用程序)的模板中,这是使用 cookie 完成的,但正如我所写的,我没有使用它们,只是使用 JWT 令牌。基本上我不希望用户在创建帐户后必须登录。
    • 我没有设置cookie,我没有设置任何与cookie相关的设置。我已经实现了 JWT 令牌创建,正如我之前写的那样,我没有使用仅 MVC 的 WebAPI。我没有会话。我对用户进行身份验证的唯一方法是通过在标头内部发送的令牌。抱歉,我没有事先写好。
    • 经典 ASP 到 ASP.Net 到 MVC 到 Web API...都使用 cookie 进行会话管理。还有一个无 cookie 会话 (msdn.microsoft.com/en-us/library/aa479314.aspx),但 OAuth 需要自定义代码才能使用它。
    • 正如我在对您的第二个答案的评论中所写的那样,我没有使用会话,因为我的前端是用 javascript 完成的。我没有使用 MVC,我的 WebAPI 端点正在返回简单的 JSON 数据。用户验证完全基于 JWT 令牌完成。所有这些都有效,我只是想避免一个额外的请求,并在与创建帐户相同的请求中获取令牌。
    • 用户验证与会话维护不同:无论您如何进行验证(使用 OAuth 交换 JWT 令牌),您都需要使用 cookie 进行会话维护。是的,即使是 JavaScript,也需要 cookie。 JWT 令牌仅用于身份验证一次,而不用于会话维护(或者换句话说,保持用户登录)。为此,OWIN 管道的 OAuth 组件设置 cookie。因此,要在您刚刚为该用户创建帐户后登录该用户,您只需在服务器上为该用户创建一个会话(再次验证他/她的诗句)。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2016-03-10
    • 1970-01-01
    • 2014-09-23
    • 2021-03-18
    • 2021-08-13
    • 2020-05-05
    • 1970-01-01
    相关资源
    最近更新 更多