gpt4 book ai didi

asp.net-mvc - 使用 JWT 实现的最小 WebAPI2 + OAuth : 401 always returned

转载 作者:行者123 更新时间:2023-12-04 12:20:04 25 4
gpt4 key购买 nike

我正在尝试实现一个简单的 WebAPI2 项目,用 JWT token 保护我的 API 函数。由于我对此很陌生,因此我主要遵循以下教程作为指导:http://bitoftech.net/2015/01/21/asp-net-identity-2-with-asp-net-web-api-2-accounts-management/其代码位于 https://github.com/tjoudeh/AspNetIdentity.WebApi , 和 http://odetocode.com/blogs/scott/archive/2015/01/15/using-json-web-tokens-with-katana-and-webapi.aspx .

编辑 #1:见底部 :解决了 client_id = null 的问题。

当然,我的实现中有几个细节发生了变化,在我学习的过程中应该是最小的,而且我目前的需求并不那么复杂:我不使用 3rd 方 JWT 或安全库(如 Thinktecture 或 Jamie Kurtz JwtAuthForWebAPI),但只是坚持使用 MS JWT 组件,我也不需要 2FA 或外部登录,因为这将是由管理员注册用户的客户端应用程序使用的公司 API。

我设法实现了一个返回 JWT token 的 API,但是当我用它向任何 protected API(当然,不 protected API 可以工作)发出请求时,该请求经常被拒绝,并显示 401-Unauthorized 错误。 api/token 上的示例请求/响应端点如下所示:

请求 :

POST http://localhost:50505/token HTTP/1.1
Host: localhost:50505
Connection: keep-alive
Content-Length: 56
Accept: application/json, text/plain, */*
Origin: http://localhost:50088
User-Agent: Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.118 Safari/537.36
Content-Type: application/x-www-form-urlencoded;charset=UTF-8
Referer: http://localhost:50088/dist/
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.8,it;q=0.6

grant_type=password&username=Zeus&password=ThePasswordHere

回复 :
HTTP/1.1 200 OK
Cache-Control: no-cache
Pragma: no-cache
Content-Length: 343
Content-Type: application/json;charset=UTF-8
Expires: -1
Server: Microsoft-IIS/8.0
Access-Control-Allow-Origin: http://localhost:50088
Access-Control-Allow-Credentials: true
X-SourceFiles: =?UTF-8?B?QzpcUHJvamVjdHNcNDViXEV4b1xJYW5pdG9yXElhbml0b3IuV2ViQXBpXHRva2Vu?=
X-Powered-By: ASP.NET
Date: Mon, 13 Apr 2015 22:16:50 GMT

{"access_token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1bmlxdWVfbmFtZSI6IlpldXMiLCJyb2xlIjoiYWRtaW5pc3RyYXRvciIsImlzcyI6Imh0dHA6Ly9sb2NhbGhvc3Q6NTA1MDUiLCJhdWQiOiIwZDQ1ZTljZWM4MzY0NmI2YTE3Mzg0N2VjOWM5NmY3ZiIsImV4cCI6MTQyOTA0OTgwOSwibmJmIjoxNDI4OTYzNDA5fQ.-GFvtEfNI7Y8tf6Ln1MpxJc4yORuf2gzksGjRbSMEnU","token_type":"bearer","expires_in":86399}

如果我检查 token (在 http://jwt.io/ 处),我会得到 JWT 有效负载的 JSON:
{
"unique_name": "Zeus",
"role": "administrator",
"iss": "http://localhost:50505",
"aud": "0d45e9cec83646b6a173847ec9c96f7f",
"exp": 1429049809,
"nbf": 1428963409
}

然而,任何具有类似 token 的请求(此处是示例模板中使用的“规范”API ValuesController),如下所示(我省略了正确发出的预检 OPTIONS CORS 请求):
GET http://localhost:50505/api/values HTTP/1.1
Host: localhost:50505
Connection: keep-alive
Accept: application/json, text/plain, */*
Origin: http://localhost:50088
User-Agent: Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.118 Safari/537.36
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1bmlxdWVfbmFtZSI6IlpldXMiLCJyb2xlIjoiYWRtaW5pc3RyYXRvciIsImlzcyI6Imh0dHA6Ly9sb2NhbGhvc3Q6NTA1MDUiLCJhdWQiOiIwZDQ1ZTljZWM4MzY0NmI2YTE3Mzg0N2VjOWM5NmY3ZiIsImV4cCI6MTQyOTA4MzAzOCwibmJmIjoxNDI4OTk2NjM4fQ.i5ik6ggSzoV2Nz-1_Od5fZVKxBpgOmEJcQN00YsG_DU
Referer: http://localhost:50088/dist/
Accept-Encoding: gzip, deflate, sdch
Accept-Language: en-US,en;q=0.8,it;q=0.6

401 失败:
HTTP/1.1 401 Unauthorized
Cache-Control: no-cache
Pragma: no-cache
Content-Type: application/json; charset=utf-8
Expires: -1
Server: Microsoft-IIS/8.0
Access-Control-Allow-Origin: http://localhost:50088
Access-Control-Allow-Credentials: true
X-AspNet-Version: 4.0.30319
X-SourceFiles: =?UTF-8?B?QzpcUHJvamVjdHNcNDViXEV4b1xJYW5pdG9yXElhbml0b3IuV2ViQXBpXGFwaVx2YWx1ZXM=?=
X-Powered-By: ASP.NET
Date: Tue, 14 Apr 2015 07:30:53 GMT
Content-Length: 61

{"message":"Authorization has been denied for this request."}

鉴于这对于像我这样的安全新手来说是一个相当复杂的话题,在接下来的内容中,我将描述我的解决方案的基本方面,以便专家们能够为我指出一个解决方案,而新手可以找到一些最新的指导.

数据层

我使用 EntityFramework 在一个单独的 DLL 项目中创建了我的数据层,包括我的 IdentityDbContext -派生数据上下文及其实体( UserAudience )。 User实体只是为名字和姓氏添加了几个字符串属性。 Audience实体用于为多个受众提供基础设施;它有一个 ID(一个由字符串属性表示的 GUID)、一个名称(仅用于提供人性化的标签)和一个 base-64 编码的共享 key 。

使用迁移我创建了数据库,并为它设置了管理员用户和测试受众。

网页接口(interface)

1. 启动模板

我创建了一个空的 WebApp 项目,包括 WebAPI 库和 用户身份验证,因为默认的身份验证模板对于我的有限目的来说过于臃肿,并且对于学习者来说有太多可移动的部分。我手动添加了所需的 NuGet 包,最后是:
EntityFramework
Microsoft.AspNet.Identity.EntityFramework
Microsoft.AspNet.Cors
Microsoft.AspNet.Identity.Owin
Microsoft.AspNet.WebApi
Microsoft.AspNet.WebApi.Client
Microsoft.AspNet.WebApi.Core
Microsoft.AspNet.WebApi.Owin
Microsoft.AspNet.WebApi.WebHost
Microsoft.Owin
Microsoft.Owin.Cors
Microsoft.Owin.Host.SystemWeb
Microsoft.Owin.Security
Microsoft.Owin.Security.Cookies
Microsoft.Owin.Security.Jwt
Microsoft.Owin.Security.OAuth
Newtonsoft.Json
Owin
System.IdentityModel.Tokens.Jwt

2. 基础设施

至于基础设施,我创建了一个相当标准的 ApplicationUserManager (在我的情况下不需要底部的提供者,但我添加了这个作为其他项目的提醒):
public class ApplicationUserManager : UserManager<User>
{
public ApplicationUserManager(IUserStore<User> store)
: base(store)
{
}

public static ApplicationUserManager Create(
IdentityFactoryOptions<ApplicationUserManager> options,
IOwinContext context)
{
var manager = new ApplicationUserManager
(new UserStore<User>(context.Get<IanitorContext>()));

manager.UserValidator = new UserValidator<User>(manager)
{
AllowOnlyAlphanumericUserNames = false,
RequireUniqueEmail = true
};

manager.PasswordValidator = new PasswordValidator
{
RequiredLength = 6,
RequireNonLetterOrDigit = true,
RequireDigit = true,
RequireLowercase = true,
RequireUppercase = true,
};

manager.UserLockoutEnabledByDefault = true;
manager.DefaultAccountLockoutTimeSpan = TimeSpan.FromMinutes(5);
manager.MaxFailedAccessAttemptsBeforeLockout = 5;

var dataProtectionProvider = options.DataProtectionProvider;
if (dataProtectionProvider != null)
{
// for email confirmation and reset password life time
manager.UserTokenProvider =
new DataProtectorTokenProvider<User>(dataProtectionProvider.Create("ASP.NET Identity"))
{
TokenLifespan = TimeSpan.FromHours(6)
};
}
return manager;
}

3. 供应商

另外,我需要一个 OAuth token 提供者:AFAIK,这里的核心方法是 GrantResourceOwnerCredentials ,根据我的商店验证收到的用户名和密码;当这成功时,我创建一个新的 ClaimsIdentity并用我想在我的 token 中发布的经过身份验证的用户的声明填充它;然后我使用它加上一些元数据属性(这里是受众 ID)来创建一个 AuthenticationTicket ,并将其传递给 context.Validated方法:
public class ApplicationOAuthProvider : OAuthAuthorizationServerProvider
{
public override Task ValidateClientAuthentication
(OAuthValidateClientAuthenticationContext context)
{
context.Validated();
return Task.FromResult<object>(null);
}

public override async Task GrantResourceOwnerCredentials
(OAuthGrantResourceOwnerCredentialsContext context)
{
// http://www.codeproject.com/Articles/742532/Using-Web-API-Individual-User-Account-plus-CORS-En
if (!context.OwinContext.Response.Headers.ContainsKey("Access-Control-Allow-Origin"))
context.OwinContext.Response.Headers.Add("Access-Control-Allow-Origin", new []{"*"});

if ((String.IsNullOrWhiteSpace(context.UserName)) ||
(String.IsNullOrWhiteSpace(context.Password)))
{
context.Rejected();
return;
}

ApplicationUserManager manager =
context.OwinContext.GetUserManager<ApplicationUserManager>();
User user = await manager.FindAsync(context.UserName, context.Password);
if (user == null)
{
context.Rejected();
return;
}

// add selected claims for building the token
ClaimsIdentity identity = new ClaimsIdentity(context.Options.AuthenticationType);
identity.AddClaim(new Claim(ClaimTypes.Name, user.UserName));
foreach (var role in manager.GetRoles(user.Id))
identity.AddClaim(new Claim(ClaimTypes.Role, role));

// add audience
// TODO: why context.ClientId is null? I would expect an audience ID
AuthenticationProperties props =
new AuthenticationProperties(new Dictionary<string, string>
{
{
ApplicationJwtFormat.AUDIENCE_PROPKEY,
context.ClientId ?? ConfigurationManager.AppSettings["audienceId"]
}
});

DateTime now = DateTime.UtcNow;
props.IssuedUtc = now;
props.ExpiresUtc = now.AddMinutes(context.Options.AccessTokenExpireTimeSpan.TotalMinutes);

AuthenticationTicket ticket = new AuthenticationTicket(identity, props);
context.Validated(ticket);
}
}

这里的第一个问题是调试时我可以看到接收到的上下文客户端 ID 为空。我不确定应该在哪里设置。这就是为什么在这里我要回退到默认观众 ID(对于我的测试目的来说足够了,一次一口吃掉大象)。

这里的另一个关键组件是 JWT token 格式化程序,它负责从票证构建 JWT token 。在我的实现中,我在其构造函数中注入(inject)了一个函数来检索我的 EF 数据上下文,因为格式化程序需要它来获取观众的 key 。所需的受众 ID 来自上述代码设置的元数据属性,用于在商店中查找 Audience实体。如果没有找到,我会退回到我的 Web.config 中定义的默认受众。 (这是我使用的测试客户端应用程序)。获得受众 key 后,我可以为 token 创建签名凭据,并将其与上下文中的数据一起使用来构建我的 JWT。
public class ApplicationJwtFormat : ISecureDataFormat<AuthenticationTicket>
{
private readonly Func<IanitorContext> _contextGetter;
private string _sIssuer;
public const string AUDIENCE_PROPKEY = "audience";

private const string SIGNATURE_ALGORITHM = "http://www.w3.org/2001/04/xmldsig-more#hmac-sha256";
private const string DIGEST_ALGORITHM = "http://www.w3.org/2001/04/xmlenc#sha256";

public string Issuer
{
get { return _sIssuer; }
set
{
if (value == null) throw new ArgumentNullException("value");
_sIssuer = value;
}
}

public ApplicationJwtFormat(Func<IanitorContext> contextGetter)
{
if (contextGetter == null) throw new ArgumentNullException("contextGetter");

_contextGetter = contextGetter;
Issuer = "http://localhost:50505";
}

public string Protect(AuthenticationTicket data)
{
if (data == null) throw new ArgumentNullException("data");

// get the audience ID from the ticket properties (as set by ApplicationOAuthProvider
// GrantResourceOwnerCredentials from its OAuth client ID)
string sAudienceId = data.Properties.Dictionary.ContainsKey(AUDIENCE_PROPKEY)
? data.Properties.Dictionary[AUDIENCE_PROPKEY]
: null;

// get audience data
Audience audience;
using (IanitorContext db = _contextGetter())
{
audience = db.Audiences.FirstOrDefault(a => a.Id == sAudienceId) ??
new Audience
{
Id = ConfigurationManager.AppSettings["audienceId"],
Name = ConfigurationManager.AppSettings["audienceName"],
Base64Secret = ConfigurationManager.AppSettings["audienceSecret"]
};
}

byte[] key = TextEncodings.Base64Url.Decode(audience.Base64Secret);

DateTimeOffset? issued = data.Properties.IssuedUtc ??
new DateTimeOffset(DateTime.UtcNow);
DateTimeOffset? expires = data.Properties.ExpiresUtc;

SigningCredentials credentials = new SigningCredentials(
new InMemorySymmetricSecurityKey(key),
SIGNATURE_ALGORITHM,
DIGEST_ALGORITHM);

JwtSecurityToken token = new JwtSecurityToken(_sIssuer,
audience.Id,
data.Identity.Claims,
issued.Value.UtcDateTime,
expires.Value.UtcDateTime,
credentials);

return new JwtSecurityTokenHandler().WriteToken(token);
}

public AuthenticationTicket Unprotect(string protectedText)
{
throw new NotImplementedException();
}
}

4. 启动

最后,将东西粘合在一起的启动代码: Global.asax代码在 Application_Start只是一个方法调用: GlobalConfiguration.Configure(WebApiConfig.Register); ,它调用典型的 WebAPI 路由设置代码,并添加了一些附加内容,以仅使用承载身份验证并返回 Camel 格式的 JSON:
    public static void Register(HttpConfiguration config)
{
// Configure Web API to use only bearer token authentication.
config.SuppressDefaultHostAuthentication();

// Use camel case for JSON data
config.Formatters.JsonFormatter.SerializerSettings.ContractResolver =
new CamelCasePropertyNamesContractResolver();

// Web API routes
config.MapHttpAttributeRoutes();

config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
}

OWIN启动配置OWIN中间件:
public partial class Startup
{
public void Configuration(IAppBuilder app)
{
HttpConfiguration config = new HttpConfiguration();

app.UseCors(CorsOptions.AllowAll);
app.UseWebApi(config);

ConfigureAuth(app);
}
}

基本配置在 ConfigureAuth方法,按照模板约定在一个单独的文件中( App_Start/Startup.Auth.cs ):这有几个用于 OAuth 和 JWT 的选项包装类。请注意,对于 JWT,我将多个受众添加到配置中,方法是从商店获取它们。在 ConfigureAuth我为 OWIN 配置依赖项,以便它可以获取所需对象的实例(EF 数据上下文以及用户和角色管理器),然后使用指定的选项设置 OAuth 和 JWT。
public partial class Startup
{
public static OAuthAuthorizationServerOptions OAuthOptions { get; private set; }
public static JwtBearerAuthenticationOptions JwtOptions { get; private set; }

static Startup()
{
string sIssuer = ConfigurationManager.AppSettings["issuer"];

OAuthOptions = new OAuthAuthorizationServerOptions
{
TokenEndpointPath = new PathString("/token"),
AuthorizeEndpointPath = new PathString("/accounts/authorize"), // not used
Provider = new ApplicationOAuthProvider(),
AccessTokenExpireTimeSpan = TimeSpan.FromHours(24),
AccessTokenFormat = new ApplicationJwtFormat(IanitorContext.Create)
{
Issuer = sIssuer
},
AllowInsecureHttp = true // do not allow in production
};

List<string> aAudienceIds = new List<string>();
List<IIssuerSecurityTokenProvider> aProviders =
new List<IIssuerSecurityTokenProvider>();

using (var context = IanitorContext.Create())
{
foreach (Audience audience in context.Audiences)
{
aAudienceIds.Add(audience.Id);
aProviders.Add(new SymmetricKeyIssuerSecurityTokenProvider
(sIssuer, TextEncodings.Base64Url.Decode(audience.Base64Secret)));
}
}

JwtOptions = new JwtBearerAuthenticationOptions
{
AllowedAudiences = aAudienceIds.ToArray(),
IssuerSecurityTokenProviders = aProviders.ToArray()
};
}

public void ConfigureAuth(IAppBuilder app)
{
app.CreatePerOwinContext(IanitorContext.Create);
app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create);
app.CreatePerOwinContext<ApplicationRoleManager>(ApplicationRoleManager.Create);

app.UseOAuthAuthorizationServer(OAuthOptions);
app.UseJwtBearerAuthentication(JwtOptions);
}
}

编辑 #1 - client_id

在查看几个示例时,我以我的 ApplicationOAuthProvider 中的这段代码结束。 :
public override Task ValidateClientAuthentication
(OAuthValidateClientAuthenticationContext context)
{
// http://bitoftech.net/2014/10/27/json-web-token-asp-net-web-api-2-jwt-owin-authorization-server/

string sClientId;
string sClientSecret;

if (!context.TryGetBasicCredentials(out sClientId, out sClientSecret))
context.TryGetFormCredentials(out sClientId, out sClientSecret);

if (context.ClientId == null)
{
context.SetError("invalid_clientId", "client_Id is not set");
return Task.FromResult<object>(null);
}

IanitorContext db = context.OwinContext.Get<IanitorContext>();
Audience audience = db.Audiences.FirstOrDefault(a => a.Id == context.ClientId);

if (audience == null)
{
context.SetError("invalid_clientId",
String.Format(CultureInfo.InvariantCulture, "Invalid client_id '{0}'", context.ClientId));
return Task.FromResult<object>(null);
}

context.Validated();
return Task.FromResult<object>(null);
}

在验证时,我进行了实际检查,以便 client_id从请求的正文中检索,在我的受众商店中查找,如果找到则进行验证。这似乎解决了上面提到的问题,所以现在我在 GrantResourceOwnerCredentials 中得到了一个非空的客户端 ID。 ;我还可以检查 JWT 内容并在 aud 下找到预期的 ID。 .但是,在使用收到的 token 传递任何请求时,我不断收到 401,例如:
HTTP/1.1 401 Unauthorized
Cache-Control: no-cache
Pragma: no-cache
Content-Type: application/json; charset=utf-8
Expires: -1
Server: Microsoft-IIS/8.0
Access-Control-Allow-Origin: http://localhost:50088
Access-Control-Allow-Credentials: true
X-AspNet-Version: 4.0.30319
X-SourceFiles: =?UTF-8?B?RDpcUHJvamVjdHNcNDViXEV4b1xJYW5pdG9yXElhbml0b3IuV2ViQXBpXGFwaVx2YWx1ZXM=?=
X-Powered-By: ASP.NET
Date: Wed, 22 Apr 2015 18:05:47 GMT
Content-Length: 61

{"message":"Authorization has been denied for this request."}

最佳答案

我自己实现了 JWT OAuth 身份验证(使用不记名 token )。
我认为你绝对可以让你的代码比你目前拥有的更轻。

这是我找到的最好的文章,其中介绍了如何使用 OAuth + JWT 保护 Web API 的基础知识。

我现在没有时间进一步讨论你的问题。祝你好运!

http://chimera.labs.oreilly.com/books/1234000001708/ch16.html#_resource_server_and_authorization_server

还 :

http://www.asp.net/web-api/overview/security/authentication-and-authorization-in-aspnet-web-api

关于asp.net-mvc - 使用 JWT 实现的最小 WebAPI2 + OAuth : 401 always returned,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/29622703/

25 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com