简体   繁体   English

从Web API中的控制器返回OAuthAuthorizatioServer生成的JWT令牌

[英]Return JWT Token generated by OAuthAuthorizatioServer from controller in Web API

Following @Taiseer Joudeh I was able to create simple POC of Web API. 在@Taiseer Joudeh之后,我能够创建简单的Web API POC。 I'm able to create new account, then log-in and call secure Web API when I add JWT token to header. 我能够创建新帐户,然后登录并在向标头添加JWT令牌时调用安全Web API。

I'd like to modify method that is responsible for creating accounts. 我想修改负责创建帐户的方法。
Right now I'm returning Create (201) code with new user object, but instead I'd like to return access token. 现在我正在返回带有新用户对象的Create(201)代码,但我想返回访问令牌。

I've found similar question but it requires creating HttpClient and doing request to OAuthAuthorizatioServer TokenEndpointPath. 我发现了类似的问题,但它需要创建HttpClient并向OAuthAuthorizatioServer TokenEndpointPath发出请求。

Second question I found requires generating temporary token that is returned to front-end, but then front-end must do additional request to server to get "real" token. 我发现的第二个问题需要生成返回到前端的临时令牌,但是前端必须向服务器发出额外请求以获得“真实”令牌。

What I'd like to do is to return login response (access_token, token_type and expires_in) when I create user account. 我想做的是在创建用户帐户时返回登录响应(access_token,token_type和expires_in)。 I want user to be authenticated when his account is created. 我希望用户在创建帐户时进行身份验证。

I'm using just Web API and JWT without any cookies. 我只使用Web API和JWT而没有任何cookie。

EDIT: My temporary solution: 编辑:我的临时解决方案:
after creating user I'm doing this: 创建用户后我正在这样做:

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);

where CustomJwtFormat comes from this awesome article . CustomJwtFormat来自这篇很棒的文章

Below is some code similar to what I'm doing in my application, which is using Asp.Net Core 1.0. 下面是一些代码,类似于我在我的应用程序中所做的,它使用的是Asp.Net Core 1.0。 Your signin and user registration will differ if you're not using 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...
        }
    }

Basically, the user is created and then signed in automatically. 基本上,创建用户然后自动登录。 I then build a JWT (add in any claims you want) and return it in the response body. 然后我构建一个JWT(添加你想要的任何声明)并将其返回到响应正文中。 On the client side (MVC and Angular JS) I get the JWT out of the response body and store it. 在客户端(MVC和Angular JS),我从响应主体中获取JWT并存储它。 It is then passed back to the server in the Authorization header of each subsequent request. 然后在每个后续请求的Authorization标头中将其传递回服务器。 Authorization policies for all server actions are based on the set of claims supplied by the JWT. 所有服务器操作的授权策略均基于JWT提供的声明集。 No cookies, no state on the server. 没有cookie,服务器上没有状态。

EDIT: I suppose you don't even need to call the signIn method in this process as you can just create the user, create a JWT, and return the JWT. 编辑:我想你甚至不需要在这个过程中调用signIn方法,因为你可以创建用户,创建一个JWT,并返回JWT。 When a user logs in on a future request you would use the Sign-In, create token, Sign-Out approach. 当用户登录将来的请求时,您将使用登录,创建令牌,注销方法。

The idea of sending a response with access_token, token_type and expires_in, when the user is created is a great idea. 在创建用户时发送带有access_token,token_type和expires_in的响应的想法是个好主意。 But, I would stick to calling the "/token" end point with HttpClient to achieve this task. 但是,我会坚持用HttpClient调用“/ token”终点来完成这个任务。 There are quite a few security checks that needs to be performed before token is generated. 在生成令牌之前,需要执行相当多的安全检查。 Since this is security I would not take any risk. 由于这是安全,我不会冒任何风险。 I feel comfortable using the libraries/ code provided by industry experts. 我很乐意使用行业专家提供的库/代码。

That said, I tried to come up with a class you can call to create the token once the user is created. 也就是说,我尝试创建一个类,您可以在创建用户后调用该类来创建令牌。 This code has been taken from the Microsoft's implementation of OAuth Authorization Server in their Katana project. 此代码取自Microsoft在Katana项目中的OAuth授权服务器实现。 You can access the source here . 您可以在此处访问源代码。 As you can see there is quite a lot happening when creating the token. 正如您所看到的,在创建令牌时会发生很多事情。

Here is the modified version of that middleware class for generating the token. 以下是用于生成令牌的中间件类的修改版本。 You have to provide the proper OAuthAuthorizationServerOptions, Context, username, password, Scopes and clientid to get an access token. 您必须提供正确的OAuthAuthorizationServerOptions,Context,username,password,Scopes和clientid才能获取访问令牌。 Please note that this is a sample implementation to guide you in the right direction. 请注意,这是一个示例实现,可指导您朝着正确的方向前进。 Please test it thoroughly if you want to use this. 如果你想使用它,请彻底测试。

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;
        }
    }
}

Please let me know if you have any questions. 请让我知道,如果你有任何问题。

Thank you, Soma. 谢谢,Soma。

I assume you are referring to the following article: http://bitoftech.net/2015/02/16/implement-oauth-json-web-tokens-authentication-in-asp-net-web-api-and-identity-2/ 我假设你指的是以下文章: http//bitoftech.net/2015/02/16/implement-oauth-json-web-tokens-authentication-in-asp-net-web-api-and-identity- 2 /

The general approach to authenticating a user in this case is 在这种情况下验证用户的一般方法是

  1. Make an HTTP call to the token endpoint 对令牌端点进行HTTP调用
  2. User Enters the credentials at the UI that is rendered 用户在呈现的UI中输入凭据
  3. IdP verifies the credentials and issues a token IdP验证凭据并颁发令牌
  4. User makes another call to an authorized endpoint and OWIN will validate this (JWT) token (as configured in the ConfigureOAuthTokenConsumption method) and if successfull will set a user session with expiry same as the token expiry. 用户再次向授权端点发出呼叫,OWIN将验证此(JWT)令牌(在ConfigureOAuthTokenConsumption方法中配置),如果成功,将设置与令牌到期时相同的到期用户会话。 The session is set using session cookies. 会话使用会话cookie设置。

Now try to understand that in general, this whole process of authentication is required because your server does not trust the user logging in to be the user the person is claiming to be. 现在尝试了解一般情况下,需要整个身份验证过程,因为您的服务器不信任登录的用户是该用户声称的用户。 However, in your case, you (or your server code) just created a user and you know for sure that the person accessing your website is the user you have just created. 但是,在您的情况下,您(或您的服务器代码)只是创建了一个用户,并且您确定访问您网站的人是您刚刚创建的用户。 In this case you don't need to validate a token to create a session for this user. 在这种情况下,您无需验证令牌即可为此用户创建会话。 Just create a session with an expiry that suits your use-case. 只需创建一个适合您的用例的会话。

The next time the user will have to log-in and prove him/herself to the server using a token but this time the user does not need to prove him/her self. 下次用户必须使用令牌登录并向服务器证明他/她自己,但这次用户不需要证明他/她自己。

Note: If you absolutely are adamant about requiring a token to log in a user who you yourself have just created using their credentials here are a couple of issues. 注意:如果您绝对坚持要求令牌登录您自己刚刚使用其凭据创建的用户,则存在一些问题。

  • You are taking on the responsibility of storing (having access to) the user credentials, which might not be with you over the lifetime of the application (in most cases you might want to act as a relying party rather than an IdP). 您承担了存储(有权访问)用户凭据的责任,这些凭据在应用程序的生命周期内可能不在您身边(在大多数情况下,您可能希望充当依赖方而不是IdP)。
  • Even if you want to then doing it is not trivial. 即使你想这样做也不是微不足道的。 You will have to make the calls to the token end point in code (server or client side) on behalf of the user, enter their credentials for them, retrieve the token, call an authenticated endpoint on your site and retrieve the session cookie all while hiding all this from the user, which probably is something you will either do if you hate yourself :) but also isn't very secure way of doing things especially when you are taking all the trouble to implement OAuth in the first place. 您必须代表用户在代码(服务器或客户端)中调用令牌端点,输入他们的凭据,检索令牌,调用站点上经过身份验证的端点并检索会话cookie将所有这些隐藏在用户之外,如果你讨厌自己,这可能就是你要做的事情:)但也不是非常安全的做事方式,特别是当你在第一时间实施OAuth时遇到麻烦。

Also, take a look at Windows Server 2016 (technical preview 5 at this time) which supports implicit grants and might take writing all this custom code off your plate if you can wait a bit for RTM. 另外,看一下支持隐式授权的Windows Server 2016(技术预览版本5),如果您可以等待RTM,可能需要将所有这些自定义代码写下来。

In an OAuth solution you as a developer are are not required to handle the cookie setting yourself. 在OAuth解决方案中, 您作为开发人员不需要自己处理cookie设置。 The cookie handling is done for you automatically by the framework. cookie处理由框架自动完成。

Also the only way to set a session is a. 设置会话的唯一方法是a。 Using session cookies or b. 使用会话cookie或b。 Use cookie-less (in the url) methods. 使用无cookie(在url中)方法。 Look at http://www.cloudidentity.com/blog/2015/02/19/introducing-adal-js-v1/ for more details on token validation and session establishment (also search the term cookie and you will know what all it's used for). 请查看http://www.cloudidentity.com/blog/2015/02/19/introducing-adal-js-v1/ ,了解有关令牌验证和会话建立的更多详细信息(同时搜索术语cookie,您将知道它的全部内容用于)。

If you start thinking about not using cookies at all not only will you have to figure out how to maintain session and do it securely without cookies but also have to re-write the token refresh code that detects and refreshes the token for you based on the presence of a session cookie. 如果您开始考虑根本不使用cookie,您不仅需要弄清楚如何维护会话并在没有cookie的情况下安全地执行,还必须重新编写令牌刷新代码,以便根据您的身份检测并刷新令牌。存在会话cookie。 (ie not a smart idea) (即不是一个聪明的主意)

I am using the exact technology stack and recently implemented token based authorization successfully. 我正在使用确切的技术堆栈和最近成功实现的基于令牌的授权。 The link I took reference from had very neatly defined the token-based auth in Web APIs. 我参考的链接已经非常巧妙地定义了Web API中基于令牌的身份验证。 A must bookmark page I must say. 我必须说的必须书签页面。 Here is the link: TOKEN BASED AUTHENTICATION IN WEB APIs . 这是链接: 基于TOKEN的WEB API认证

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM