Я пытаюсь реализовать сервер OpenID OAuth 2.0 с помощью ASP.NET Framework 4.7.2 Web API.Он будет использоваться для защиты API ресурсов с помощью токенов доступа / обновления JWT.
Я довольно новичок в OpenID и OAuth, поэтому я ищу совет / руководящие указания / libray, которые можно использовать для реализации этого сервера авторизации.
Сервер аутентификации должен быть реализован с использованием ASP.NET Framework 4.7.2, для Core в настоящее время нет опций.API ресурсов будут написаны на ASP.NET Core 2.X.
Я следовал замечательным учебникам Taiseer ( Часть 1 , Часть 3 , Part 5 и JWT Setup ), и в настоящее время у него есть сервер OAuth, который может генерировать токены JWT, и Core API, который может проверять токен.
Вот код, который яВ настоящее время есть.
Сервер аутентификации:
Startup.cs
public class Startup
{
public void Configuration(IAppBuilder app)
{
ConfigureOAuth(app);
HttpConfiguration config = new HttpConfiguration();
WebApiConfig.Register(config);
app.UseWebApi(config);
}
public void ConfigureOAuth(IAppBuilder app)
{
app.CreatePerOwinContext<SecurityUserManager>(SecurityUserManager.Create);
OAuthAuthorizationServerOptions OAuthServerOptions = new OAuthAuthorizationServerOptions()
{
AllowInsecureHttp = true,
TokenEndpointPath = new PathString("/oauth2/token"),
AccessTokenExpireTimeSpan = TimeSpan.FromMinutes(30),
Provider = new SmAuthorizationServerProvider(),
RefreshTokenProvider = new SmRefreshTokenProvider(),
AccessTokenFormat = new SmJwtFormat("http://localhost:7814"),
ApplicationCanDisplayErrors = true,
};
// Token Generation
app.UseOAuthAuthorizationServer(OAuthServerOptions);
app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions());
}
}
SmAuthorizationServerProvider.cs
public class SmAuthorizationServerProvider : OAuthAuthorizationServerProvider
{
public override Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)
{
string clientId = string.Empty;
string clientSecret = string.Empty;
Client client = null;
if (!context.TryGetBasicCredentials(out clientId, out clientSecret))
{
context.TryGetFormCredentials(out clientId, out clientSecret);
}
if (context.ClientId == null)
{
//Remove the comments from the below line context.SetError, and invalidate context
//if you want to force sending clientId/secrects once obtain access tokens.
context.Validated();
//context.SetError("invalid_clientId", "ClientId should be sent.");
return Task.FromResult<object>(null);
}
using (AuthRepository _repo = new AuthRepository())
{
client = _repo.FindClient(context.ClientId);
}
if (client == null)
{
context.SetError("invalid_clientId", string.Format("Client '{0}' is not registered in the system.", context.ClientId));
return Task.FromResult<object>(null);
}
if (client.ApplicationType == Models.ApplicationTypes.NativeConfidential)
{
if (string.IsNullOrWhiteSpace(clientSecret))
{
context.SetError("invalid_clientId", "Client secret should be sent.");
return Task.FromResult<object>(null);
}
else
{
if (client.Secret != Helper.GetHash(clientSecret))
{
context.SetError("invalid_clientId", "Client secret is invalid.");
return Task.FromResult<object>(null);
}
}
}
if (!client.Active)
{
context.SetError("invalid_clientId", "Client is inactive.");
return Task.FromResult<object>(null);
}
context.OwinContext.Set<string>("as:clientAllowedOrigin", client.AllowedOrigin);
context.OwinContext.Set<string>("as:clientRefreshTokenLifeTime", client.RefreshTokenLifeTime.ToString());
context.Validated();
return Task.FromResult<object>(null);
}
public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
{
var allowedOrigin = context.OwinContext.Get<string>("as:clientAllowedOrigin");
SecurityUser user = null;
if (allowedOrigin == null) allowedOrigin = "*";
context.OwinContext.Response.Headers.Add("Access-Control-Allow-Origin", new[] { allowedOrigin });
using (AuthRepository _repo = new AuthRepository())
{
user = await _repo.FindUser(context.UserName, context.Password);
if (user == null)
{
context.SetError("invalid_grant", "The user name or password is incorrect.");
return;
}
}
string scopes = null;
if (context.Scope.Count > 0)
{
scopes = string.Join(" ", context.Scope.Select(x => x.ToString()).ToArray());
}
var identity = new ClaimsIdentity(context.Options.AuthenticationType);
// Add the user id as claim here
// Keep the claims number small, the token length increases with each new claim
identity.AddClaim(new Claim("sid", user.Id.ToString()));
// add the client id as claim
if (!string.IsNullOrEmpty(context.ClientId))
{
identity.AddClaim(new Claim("client_id", context.ClientId));
}
var props = new AuthenticationProperties(new Dictionary<string, string>
{
{
"as:client_id", (context.ClientId == null) ? string.Empty : context.ClientId
},
{
"as:scope", (scopes == null) ? string.Empty : scopes
},
{
"userName", context.UserName
}
});
var ticket = new AuthenticationTicket(identity, props);
context.Validated(ticket);
}
public override Task TokenEndpoint(OAuthTokenEndpointContext context)
{
foreach (KeyValuePair<string, string> property in context.Properties.Dictionary)
{
context.AdditionalResponseParameters.Add(property.Key, property.Value);
}
return Task.FromResult<object>(null);
}
public override Task GrantRefreshToken(OAuthGrantRefreshTokenContext context)
{
var originalClient = context.Ticket.Properties.Dictionary["as:client_id"];
var currentClient = context.ClientId;
// check if the token is created with specified client_id
if (!string.IsNullOrEmpty(currentClient) && !string.IsNullOrEmpty(originalClient))
{
if (originalClient != currentClient)
{
context.SetError("invalid_clientId", "Refresh token is issued to a different clientId.");
return Task.FromResult<object>(null);
}
}
var newIdentity = new ClaimsIdentity(context.Ticket.Identity);
// Change auth ticket for refresh token requests if needed
// newIdentity.AddClaim(new Claim("newClaim", "newValue"));
var newTicket = new AuthenticationTicket(newIdentity, context.Ticket.Properties);
context.Validated(newTicket);
return Task.FromResult<object>(null);
}
public override Task GrantClientCredentials(OAuthGrantClientCredentialsContext context)
{
Client client;
using (AuthRepository _repo = new AuthRepository())
{
client = _repo.FindClient(context.ClientId);
}
var oAuthIdentity = new ClaimsIdentity(context.Options.AuthenticationType);
oAuthIdentity.AddClaim(new Claim("client_id", client.Id));
var props = new AuthenticationProperties(new Dictionary<string, string>
{
{
"as:client_id", (context.ClientId == null) ? string.Empty : context.ClientId
}
});
var ticket = new AuthenticationTicket(oAuthIdentity, props);
context.Validated(ticket);
return base.GrantClientCredentials(context);
}
}
SmJwtFormat.cs
public class SmJwtFormat : ISecureDataFormat<AuthenticationTicket>
{
private const string AudiencePropertyKey = "as:scope";
private readonly string _issuer = string.Empty;
private AuthRepository authRepo;
private SecurityKey signingKey;
private string secret = "P@ssw0rd-7BBF8546-C8C1-44D9-A404-9E1CAF80EC9D-F2FEC38D-2041-499E-9FAA-218C8B1EEC7B";
public SmJwtFormat(string issuer)
{
_issuer = issuer;
authRepo = new AuthRepository();
// Generating the signingKey
string symmetricKeyAsBase64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(secret));
var keyByteArray = TextEncodings.Base64Url.Decode(symmetricKeyAsBase64);
signingKey = new SymmetricSecurityKey(keyByteArray);
}
public string Protect(AuthenticationTicket data)
{
if (data == null)
{
throw new ArgumentNullException("data");
}
// The token audience from the JWT terminology is the same as the token Scope in OAuth terminology.
string scope = data.Properties.Dictionary.ContainsKey(AudiencePropertyKey) ? data.Properties.Dictionary[AudiencePropertyKey] : null;
if (string.IsNullOrWhiteSpace(scope)) throw new InvalidOperationException("AuthenticationTicket.Properties does not include audience/scope");
var signingCredentials = new SigningCredentials(signingKey, SecurityAlgorithms.HmacSha256Signature);
var issued = data.Properties.IssuedUtc;
var expires = data.Properties.ExpiresUtc;
if (scope != null)
{
var scopesList = scope.Split(' ').ToList();
var audClaims = scopesList.Select(s => new Claim("aud", s));
data.Identity.AddClaims(audClaims);
}
var token = new JwtSecurityToken(_issuer, null, data.Identity.Claims, issued.Value.UtcDateTime, expires.Value.UtcDateTime, signingCredentials);
var handler = new JwtSecurityTokenHandler();
var jwt = handler.WriteToken(token);
return jwt;
}
public AuthenticationTicket Unprotect(string protectedText)
{
var tokenValidationParameters = new TokenValidationParameters
{
ValidIssuer = _issuer,
IssuerSigningKey = signingKey,
};
var handler = new JwtSecurityTokenHandler();
SecurityToken token = null;
// Unpack token
var pt = handler.ReadJwtToken(protectedText);
string t = pt.RawData;
var principal = handler.ValidateToken(t, tokenValidationParameters, out token);
var identity = principal.Identities;
return new AuthenticationTicket(identity.First(), new AuthenticationProperties());
}
}
Вот мой ресурс ASP.NET Core 2.2 API Startup.cs
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
services.AddAuthorization();
// identity http://localhost:7814
// resource https://localhost:44337
var key = "P@ssw0rd-7BBF8546-C8C1-44D9-A404-9E1CAF80EC9D-F2FEC38D-2041-499E-9FAA-218C8B1EEC7B";
string symmetricKeyAsBase64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(key));
var keyByteArray = Convert.FromBase64String(symmetricKeyAsBase64);
var securityKey = new SymmetricSecurityKey(keyByteArray);
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(o =>
{
o.Authority = "http://localhost:7814/";
o.RequireHttpsMetadata = false;
o.TokenValidationParameters = new TokenValidationParameters()
{
ValidateIssuer = true,
ValidIssuer = "http://localhost:7814",
ValidateAudience = true,
ValidAudiences = new List<string>()
{
"api1"
},
//IssuerSigningKey = securityKey
};
});
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseAuthentication();
app.UseMvc();
}
}
В настоящее время я могу генерировать токены, отправив сообщение http://localhost:7814/oauth2/token со следующими параметрами:
grant_type = пароль
имя пользователя = user1
пароль = p @ ssw0rd
client_id = smLocalhost
client_secret = secret
scope = api1 api2
После этого токен можно использовать для доступа к защищенным конечным точкам в API ресурсов.
Я исследовал IdentityServer4 , OpenIdDict и AspNet.Security.OpenIdConnect.Server , но, похоже, они работают только с ASP.NET Core.
Итак, после всего этого, мои вопросы:
1.Как добавить OpenID поверх этого?
2.Есть ли библиотека, которую я могу использовать?
3.Не могли бы вы дать мне учебник / совет, что я могу сделать для его реализации?
4.Как реализовать конечную точку документа обнаружения (.well-known / openid-configuration) и повернуть открытые ключи для асимметричной подписи токена?
Заранее спасибо!