我得到 cookie 而不是带有授权码授予的令牌

I get cookie instead of token with authorization code grant

总结

我有 ASP.NET 具有身份验证功能的 MVC 5 Web 应用程序,我必须使用 "grant_type" = "authorization_code" 开发一个 API。此 API 将向另一个需要自定义错误响应的“well-known”网络服务提供用户数据。我的 IDE 是 Visual Studio Professional 2017。我使用 Postman 向我的 Web API.

发出请求

我阅读的文档

OWIN and Katana documentation the OWIN OAuth 2.0 Authorization Server link redirects again to main OWIN and Katana page, but I think that I found the source on GitHub: OWIN OAuth 2.0 Authorization Server。我试着按照这个文档,但是没有关于这个问题的例子。

问题

当用户验证并授权“well-known”Web 服务客户端时,我可以在我的 AuthorizationCodeProvider class(使用 Create() 方法)中创建一个新的授权代码访问用户的资源。我将此代码存储在数据库中。当我请求 Token 时,调用 AuthorizationCodeProvider.Receive() 方法并且令牌被正确反序列化。然后调用 GrantAuthorizationCode() 方法,Postman 收到 OK 响应(200 状态码)但 body 中没有令牌信息(.AspNet.ApplicationCookie 在 cookie 中)。

详细解释及代码

这是Startup class:

public partial class Startup
{
    public static OAuthAuthorizationServerOptions OAuthOptions { get; private set; }

    public void ConfigureAuth(IAppBuilder app)
    {
        app.CreatePerOwinContext(ApplicationDbContext.Create);
        app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create);
        app.CreatePerOwinContext<ApplicationSignInManager>(ApplicationSignInManager.Create);
        app.UseCookieAuthentication(new CookieAuthenticationOptions
        {
            AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
            LoginPath = new PathString("/Account/Login"),
            Provider = new CookieAuthenticationProvider
            {
                OnValidateIdentity = SecurityStampValidator.OnValidateIdentity<ApplicationUserManager, ApplicationUser>(
                    validateInterval: TimeSpan.FromMinutes(30),
                    regenerateIdentity: (manager, user) => user.GenerateUserIdentityAsync(manager)),
                OnApplyRedirect = (context =>
                {
                    // This code is to return custom error response
                    string path = null;
                    if (context.Request.Path.HasValue)
                        path = context.Request.Path.Value;
                    if (!(path != null && path.Contains("/api"))) // Don't redirect to login page
                        context.Response.Redirect(context.RedirectUri);
                })
            }
        });            
        app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);
        app.UseTwoFactorSignInCookie(DefaultAuthenticationTypes.TwoFactorCookie, TimeSpan.FromMinutes(5));         
        app.UseTwoFactorRememberBrowserCookie(DefaultAuthenticationTypes.TwoFactorRememberBrowserCookie);

        this.ConfigureAuthorization(app);
    }
    
    private void ConfigureAuthorization(IAppBuilder app)
    {
        app.UseCors(Microsoft.Owin.Cors.CorsOptions.AllowAll);

        OAuthOptions = new OAuthAuthorizationServerOptions
        {
            AllowInsecureHttp = false,
            TokenEndpointPath = new PathString("/api/token"),
            AccessTokenExpireTimeSpan = TimeSpan.FromDays(1),
            Provider = new TokenAuthorizationServerProvider(),
            AuthorizationCodeProvider = new AuthorizationCodeProvider()
        };            
        app.Use<AuthenticationMiddleware>(); //Customize responses in Token middleware
        app.UseOAuthAuthorizationServer(OAuthOptions);
        app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions());
    }
}

ConfigureAuthorization()方法配置授权。它使用我实现的 classes:

AuthenticationMiddleware:well-known Web 服务需要带有自定义错误 JONS 的 401 状态响应,而不是通常的 400 状态响应。它基于问题 Replace response body using owin middleware.

的答案
public class AuthenticationMiddleware : OwinMiddleware
{
    public AuthenticationMiddleware(OwinMiddleware next) : base(next) { }

    public override async Task Invoke(IOwinContext context)
    {
        var owinResponse = context.Response;
        var owinResponseStream = owinResponse.Body;
        var responseBuffer = new MemoryStream();
        owinResponse.Body = responseBuffer;

        await Next.Invoke(context);

        if (context.Response.StatusCode == (int)HttpStatusCode.BadRequest &&
            context.Response.Headers.ContainsKey(BearerConstants.CustomUnauthorizedHeaderKey))
        {
            context.Response.StatusCode = (int)HttpStatusCode.Unauthorized;

            string headerValue = context.Response.Headers.Get(BearerConstants.CustomUnauthorizedHeaderKey);
            context.Response.Headers.Remove(BearerConstants.CustomUnauthorizedHeaderKey);

            ErrorMessage errorMessage = new ErrorMessage(headerValue);
            string json = JsonConvert.SerializeObject(errorMessage, Formatting.Indented);

            var customResponseBody = new StringContent(json);
            var customResponseStream = await customResponseBody.ReadAsStreamAsync();
            await customResponseStream.CopyToAsync(owinResponseStream);

            owinResponse.ContentType = "application/json";
            owinResponse.ContentLength = customResponseStream.Length;
            owinResponse.Body = owinResponseStream;
        }
    }
}

ErrorMessage序列化为JSONreturns数组时出现错误:

{
  "errors":
  [
    "message": "the error message"
  ]
}

我使用扩展方法在 TokenAuthorizationServerProvider.ValidateClientAuthentication() 方法中设置了 BearerConstants.CustomUnauthorizedHeaderKey header:

public static void Rejected(this OAuthValidateClientAuthenticationContext context, string message)
{
    Debug.WriteLine($"\t\t{message}");
    context.SetError(message);
    context.Response.Headers.Add(BearerConstants.CustomUnauthorizedHeaderKey, new string[] { message });
    context.Rejected();
}

这是 TokenAuthorizationServerProvider 的实现方式:

public class TokenAuthorizationServerProvider : OAuthAuthorizationServerProvider
{
    public override Task AuthorizeEndpoint(OAuthAuthorizeEndpointContext context)
    {
        // Only for breakpoint. Never stops.
        return base.AuthorizeEndpoint(context);
    }

    public override async Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)
    {
        // Check if grant_type is authorization_code
        string grantType = context.Parameters[BearerConstants.GrantTypeKey];
        if (string.IsNullOrEmpty(grantType) || grantType != BearerConstants.GrantTypeAuthorizationCode)
        {
            context.Rejected("Invalid grant type"); // Sets header for custom response
            return;
        }

        // Check if client_id and client_secret are in the request
        string clientId = context.Parameters[BearerConstants.ClientIdKey];
        string clientSecret = context.Parameters[BearerConstants.ClientSecretKey];
        if (string.IsNullOrEmpty(clientId) || string.IsNullOrEmpty(clientSecret))
        {
            context.Rejected("Client credentials missing"); // Sets header for custom response
            return;
        }

        //Check if client_id and client_secret are valid
        ApiClient client = await (new ApiClientService()).ValidateClient(clientId, clientSecret);
        if (client != null)
        {
            // Client has been verified.
            Debug.WriteLine($"\t\tClient has been verified");
            context.OwinContext.Set<ApiClient>("oauth:client", client);
            context.Validated(clientId);
        }
        else
        {
            // Client could not be validated.
            context.Rejected("Invalid client"); // Sets header for custom response
        }
    }

    public override async Task GrantAuthorizationCode(OAuthGrantAuthorizationCodeContext context)
    {
        TokenRequestParameters parameters = await context.Request.GetBodyParameters();

        using (IUserService userService = new UserService())
        {
            ApplicationUser user = await userService.ValidateUser(parameters.Code);
            if (user == null)
            {
                context.Rejected("Invalid code");
                return;
            }
            // Initialization.  
            var claims = new List<Claim>();

            // Setting  
            claims.Add(new Claim(ClaimTypes.Name, user.UserName));

            // Setting Claim Identities for OAUTH 2 protocol.  
            ClaimsIdentity oAuthClaimIdentity = new ClaimsIdentity(claims, OAuthDefaults.AuthenticationType);
            ClaimsIdentity cookiesClaimIdentity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationType);

            // Setting user authentication.
            IDictionary<string, string> data = new Dictionary<string, string>{ { "userName", user.UserName } };
            AuthenticationProperties properties = new AuthenticationProperties(data);
            AuthenticationTicket ticket = new AuthenticationTicket(oAuthClaimIdentity, properties);

            // Grant access to authorize user.  
            context.Validated(ticket);
            context.Request.Context.Authentication.SignIn(cookiesClaimIdentity);
        }
    }
}

ApiClientService.ValidateClient() 在数据库中检查客户端 ID 和密码是否正确。

GrantAuthorizationCode() 基于 ASP.NET MVC - OAuth 2.0 REST Web API Authorization Using Database First Approach 教程中的第 8 步。但是 grant_type = password 的教程,我认为这里有问题。

AuthorizationCodeProvider class:

public class AuthorizationCodeProvider : AuthenticationTokenProvider
{
    public override void Create(AuthenticationTokenCreateContext context)
    {
        AuthenticationTicket ticket = context.Ticket;            
        string serializedTicket = context.SerializeTicket();
        context.SetToken(serializedTicket);
    }

    public override void Receive(AuthenticationTokenReceiveContext context)
    {
        context.DeserializeTicket(context.Token);
        // At this point context.Ticket.Identity.IsAuthenticated is true
    }
}

我从显示 Allow/Deny 视图的 AuthorizationController 调用创建方法。它装饰有 System.Web.Mvc.Authorize 属性,因此如果用户未通过身份验证,他或她必须使用来自 MVC 模板项目 (/account/login):

的默认登录页面登录
[Authorize]
public class AuthorizationController : Controller
{
    private const string ServiceScope = "service-name";

    [HttpGet]
    public async Task<ActionResult> Index(string client_id, string response_type, string redirect_uri, string scope, string state)
    {
        AuthorizationViewModel vm = new AuthorizationViewModel()
        {
            ClientId = client_id,
            RedirectUri = redirect_uri,
            Scope = scope,
            State = state
        };

        if (scope == ServiceScope)
        {
            var authentication = HttpContext.GetOwinContext().Authentication;
            authentication.SignIn(
                new AuthenticationProperties { IsPersistent = true, RedirectUri = redirect_uri },
                new ClaimsIdentity(new[] { new Claim(ClaimsIdentity.DefaultNameClaimType, User.Identity.Name) },
                "Bearer"));
        }

        return View(vm);
    }

    [HttpPost]
    [ValidateAntiForgeryToken]
    [MultiButton(MatchFormKey = "authorization", MatchFormValue = "Allow")]
    public async Task<ActionResult> Allow(AuthorizationViewModel vm)
    {
        if (ModelState.IsValid)
        {
            string code = await this.SetAuthorizationCode(vm.ClientId, vm.RedirectUri);
            if (vm.Scope == ServiceScope)
            {
                string url = $"{vm.RedirectUri}?code={code}&state={vm.State}";
                return Redirect(url);
            }
            else
            {
                return Redirect(vm.RedirectUri);
            }
        }
        return View(vm);
    }

    [HttpPost]
    [ValidateAntiForgeryToken]
    [MultiButton(MatchFormKey = "authorization", MatchFormValue = "Deny")]
    public async Task<ActionResult> Deny(AuthorizationViewModel vm)
    {
        // Removed for brevity
        return View(vm);
    }

    private async Task<string> SetAuthorizationCode(string clientId, string redirectUri)
    {
        string userId = User.Identity.GetUserId();
        ClaimsIdentity identity = new ClaimsIdentity(new GenericIdentity(clientId, OAuthDefaults.AuthenticationType));
        AuthenticationTokenCreateContext authorizeCodeContext = new AuthenticationTokenCreateContext(
            HttpContext.GetOwinContext(),
            Startup.OAuthOptions.AuthorizationCodeFormat,
            new AuthenticationTicket(
                identity,
                new AuthenticationProperties(new Dictionary<string, string>
                {
                    { "user_id", userId },
                    { "client_id", clientId },
                    { "redirect_uri", redirectUri }
                })
                {
                    IssuedUtc = DateTimeOffset.UtcNow,
                    ExpiresUtc = DateTimeOffset.UtcNow.Add(Startup.OAuthOptions.AuthorizationCodeExpireTimeSpan)
                }));

        Startup.OAuthOptions.AuthorizationCodeProvider.Create(authorizeCodeContext);
        string code = authorizeCodeContext.Token;

        IUserService userService = new UserService();
        await userService.SetAuthorization(userId, true, code); // save to database
        userService.Dispose();

        return code;
    }
}

授权码在SetAuthorizationCode()方法中创建,在Allow()动作中调用。此 SetAuthorizationCode() 方法代码基于 this answer.

问题

我现在很长,有很多代码,但我卡了几天,我没有找到解决方案。我不知道授权的完整流程,我想我错过了什么。

我找到了问题的解决方案,它是 AuthenticationMiddleware。一旦响应的主体被读取,它仍然是空的并且不会到达客户端。所以你必须重写响应体。

public class AuthenticationMiddleware : OwinMiddleware
{
    public AuthenticationMiddleware(OwinMiddleware next) : base(next) { }
    
    public override async Task Invoke(IOwinContext context)
    {
        var owinResponse = context.Response;
        var owinResponseStream = owinResponse.Body;
        var responseBuffer = new MemoryStream();
        owinResponse.Body = responseBuffer;
        
        await Next.Invoke(context);
        
        if (context.Response.StatusCode == (int)HttpStatusCode.BadRequest &&
            context.Response.Headers.ContainsKey(BearerConstants.CustomUnauthorizedHeaderKey))
        {
            // Customize the response
        }
        else
        {
            // Set body again with the same content
            string body = Encoding.UTF8.GetString(responseBuffer.ToArray());
            StringContent customResponseBody = new StringContent(body);
            Stream customResponseStream = await customResponseBody.ReadAsStreamAsync();
            await customResponseStream.CopyToAsync(owinResponseStream);
        }
    }
}