Summary
I have ASP.NET MVC 5 web app with Identity authentication and I have to develop an API with "grant_type" = "authorization_code". This API will be to provide users data to another "well-known" web service that needs a custom error responses. My IDE is Visual Studio Professional 2017. I use Postman to make requests to my Web API.
Documentation I read
In the 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. I tried to follow this documentation, but there are no examples about this question.
Problem
I can create a new authorization code in my AuthorizationCodeProvider class (with Create() method) when a user authenticates and authorizes the "well-known" web service client to access user's resources. I store this code in a database. When I request a Token AuthorizationCodeProvider.Receive() method is called and the token is deserialized correctly. Then GrantAuthorizationCode() method is called, Postman receives OK response (200 status code) but without token information in body (.AspNet.ApplicationCookie is in cookies).
Detailed explanation and code
This is the 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() method configures the authorization. It uses classes implemented by me:
AuthenticationMiddleware: the well-known web service wants 401 status responses with custom error JONS instead of the usual 400 status response. It is based on the answer of the question 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;
}
}
}
When ErrorMessage is serialized to JSON returns an array of errors:
{
"errors":
[
"message": "the error message"
]
}
I set the BearerConstants.CustomUnauthorizedHeaderKey header in TokenAuthorizationServerProvider.ValidateClientAuthentication() method using a extension method:
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();
}
This is how TokenAuthorizationServerProvider is implemented:
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() checks on database that cliend ID and Secret are correct.
GrantAuthorizationCode() is based on the step 8 from ASP.NET MVC - OAuth 2.0 REST Web API Authorization Using Database First Approach tutorial. But this tutorial for grant_type = password and I think that something is wrong in here.
And the 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
}
}
I call to create method from the AuthorizationController that shows the Allow/Deny view. It is decorated with System.Web.Mvc.Authorize attribute, so if the user isn't authenticated he or she has to login using the default login page from MVC template project (/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;
}
}
The authorization code is created in SetAuthorizationCode() method, which is called in Allow() action. This SetAuthorizationCode() method code is based on this answer.
Questions
I now that is very long with a lot of code, but I'm stuck for some days and I didn't find the solution. I don't know the complete flow of the authorization, I think that I'm missing something.
What happens when I call /api/token? I mean, what are the steps in this part of the authentication/authorization flow?
What happens after AuthorizationCodeProvider.GrantAuthorizationCode()?
Why a cookie returned instead of token in the body?
I found the solution of the problem, it was the AuthenticationMiddleware. Once the body of the response is read, it remains empty and does not reach the client. So you have to rewrite the response body.
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);
}
}
}
Related
I have a minimal api here. It creates me a JWT for authorization, and then it tests it. using the [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] attribute.
app.MapPost("/login", [AllowAnonymous]
async (HttpContext http, ITokenService tokenService, IUserRepositoryService userRepositoryService) =>
{
var userLogin = await http.Request.ReadFromJsonAsync<UserModel>();
var userDto = userRepositoryService.GetUser(userLogin);
if (userDto == null)
{
http.Response.StatusCode = 401;
return;
}
var token = tokenService.BuildToken(builder.Configuration["Jwt:Key"], builder.Configuration["Jwt:Issuer"],
builder.Configuration["Jwt:Audience"], userDto);
await http.Response.WriteAsJsonAsync(new { Token = token });
});
app.MapGet("/secretAction",
(Func<string>)([Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]() => "Action Succeeded")
);
The first method works just fine. A post to the login endpoint with a json string containing the login and password returns to me a jwt.
The second endpoint is always returning unauthorized.
I have been back and forth with this trying to understand why its not able to parse the token that it creates.
Any help would be greatly appreciated.
appsettings.json
"Jwt": {
"Key": "this-is-the-secret",
"Issuer": "https://jwtauth.example.com",
"Audience": "api1"
}
program.cs
using System.ComponentModel.DataAnnotations;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.IdentityModel.Tokens;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<ITokenService>(new TokenService());
builder.Services.AddSingleton<IUserRepositoryService>(new UserRepositoryService());
builder.Services.AddAuthorization();
var key = Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"]);
builder.Services
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(opt =>
{
opt.RequireHttpsMetadata = false;
opt.SaveToken = true;
opt.TokenValidationParameters = new TokenValidationParameters
{
IssuerSigningKey = new SymmetricSecurityKey(key),
ValidateIssuerSigningKey = true,
ValidIssuer = builder.Configuration["Jwt:Issuer"],
ValidateIssuer = false,
ValidAudience = builder.Configuration["Jwt:Audience"],
ValidateAudience = true,
ValidateLifetime = true,
};
});
await using var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.MapGet("/",
(Func<string>)(() =>
"Test JWT Authentication using Minimalist Web API .net 6. <br> /login UserName: user1, Password: test <br> /secretAction "));
app.MapPost("/login", [AllowAnonymous]
async (HttpContext http, ITokenService tokenService, IUserRepositoryService userRepositoryService) =>
{
var userLogin = await http.Request.ReadFromJsonAsync<UserModel>();
var userDto = userRepositoryService.GetUser(userLogin);
if (userDto == null)
{
http.Response.StatusCode = 401;
return;
}
var token = tokenService.BuildToken(builder.Configuration["Jwt:Key"], builder.Configuration["Jwt:Issuer"],
builder.Configuration["Jwt:Audience"], userDto);
await http.Response.WriteAsJsonAsync(new { Token = token });
});
app.MapGet("/secretAction",
(Func<string>)([Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]() => "Action Succeeded")
);
await app.RunAsync();
public record UserDto(string UserName, string Password);
public record UserModel
{
[Required] public string UserName { get; set; }
[Required] public string Password { get; set; }
}
public interface IUserRepositoryService
{
UserDto GetUser(UserModel userModel);
}
public class UserRepositoryService : IUserRepositoryService
{
private List<UserDto> _users => new()
{
new("User1", "test"),
};
public UserDto GetUser(UserModel userModel)
{
return _users.FirstOrDefault(x =>
string.Equals(x.UserName, userModel.UserName) && string.Equals(x.Password, userModel.Password));
}
}
public interface ITokenService
{
string BuildToken(string key, string issuer, string audience, UserDto user);
}
public class TokenService : ITokenService
{
private TimeSpan ExpiryDuration = new TimeSpan(0, 30, 0);
public string BuildToken(string key, string issuer, string audience, UserDto user)
{
var keyBytes = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(key));
var claims = new[]
{
new Claim(ClaimTypes.Name, user.UserName),
new Claim(ClaimTypes.NameIdentifier, Guid.NewGuid().ToString())
};
var descriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(claims),
Issuer = issuer,
Audience = audience,
SigningCredentials = new SigningCredentials(keyBytes, SecurityAlgorithms.HmacSha256Signature),
IssuedAt = DateTime.Now,
NotBefore = DateTime.Now,
Expires = DateTime.Now.AddDays(1)
};
var jwtHandler = new JwtSecurityTokenHandler();
var token = jwtHandler.CreateToken(descriptor);
return jwtHandler.WriteToken(token);
// alternatively
// return jwtHandler.CreateEncodedJwt(descriptor);
}
To test it.
const string jwtUrl = "https://localhost:7080/login";
var content = new StringContent("{\"UserName\" : \"User1\",\"Password\" : \"test\"}", Encoding.UTF8, "application/json");
var httpResponseMessage = await client.PostAsync(jwtUrl,content);
var jwt = await httpResponseMessage.Content.ReadAsStringAsync();
var queueMessage = JsonSerializer.Deserialize<TokenResponse>(jwt);
Console.WriteLine(queueMessage.token);
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", queueMessage.token);
const string protectedUrl = "https://localhost:7080/secretAction";
var result = await client.GetStringAsync(protectedUrl);
Console.WriteLine(result);
Logs
Here is a JWT just created.
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiVXNlcjEiLCJuYW1laWQiOiI2OWJmOTg5Ni0wYTIyLTQ1N2UtODkyMy00ZTM4MGQzMTEyNTkiLCJuYmYiOjE2NjcyOTA5NTgsImV4cCI6MTY2NzM3NzM1OCwiaWF0IjoxNjY3MjkwOTU4LCJpc3MiOiJodHRwczovL2p3dGF1dGguZXhhbXBsZS5jb20i
LCJhdWQiOiJhcGkxIn0.aUxhJuOrNOHeId6vHpHe1ZqnuC2MJ4TaYi577Cc37oU
The logs from trying to access the secretAction sending the jwt as a bearer token.
System.Net.Http.HttpRequestException: Response status code does not indicate success: 401 (Unauthorized).
at System.Net.Http.HttpResponseMessage.EnsureSuccessStatusCode()
at System.Net.Http.HttpClient.GetStringAsyncCore(HttpRequestMessage request, CancellationToken cancellationToken)
at Program.$(String[] args) in C:\Development\FreeLance\Glassix\asp-net-core-auth-with-self-generated-jwt\ConsoleApp1\Program.cs:line 27
So with some hints from friends on twitter. and Khellang I added event logging to the error
opt.Events = new JwtBearerEvents()
{
OnAuthenticationFailed = context =>
{
var err = context.Exception.ToString();
return context.Response.WriteAsync(err);
}
};
This lead to the error message
Method not found: 'Void Microsoft.IdentityModel.Tokens.InternalValidators.ValidateLifetimeAndIssuerAfterSignatureNotValidatedJwt
Followed by this question on Stack Unauthorized (Invalid Token) when authenticating with JWT Bearer Token after update to .NET 6
After installing the recommended package System.IdentityModel.Tokens.Jwt everything magically works.
I have an ASP.Net Web API 2 with BasicAuthenticationAttribute that is working as expected. In my application, there are different controllers and I want to add bearer token-based authentication to one of my controllers. I added those NuGet packages:
Microsoft.AspNet.WebApi.Owin
Microsoft.Owin.Host.SystemWeb
Microsoft.Owin.Security.OAuth
Here is the WebApiConfig:
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
config.Formatters.JsonFormatter.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore;
config.Formatters.JsonFormatter.SerializerSettings.DateTimeZoneHandling =
DateTimeZoneHandling.Local;
config.MapHttpAttributeRoutes();
config.Routes.MapHttpRoute(
"DefaultApi",
"api/{controller}/{id}",
new {id = RouteParameter.Optional}
);
config.MessageHandlers.Add(new RequestResponseHandler());
config.Filters.Add(new CustomExceptionFilter());
var resolver = config.DependencyResolver; //Assuming one is set.
var basicAuth = (BasicAuthenticationAttribute)resolver.GetService(typeof(BasicAuthenticationAttribute));
// Web API configuration and services
if (basicAuth != null) config.Filters.Add(basicAuth);
}
}
Here is the Owin Startup
public class Startup
{
public void Configuration(IAppBuilder app)
{
var configuration = GlobalConfiguration.Configuration;
WebApiConfig.Register(configuration);
app.UseWebApi(configuration);
Configure(app);
}
private static void Configure(IAppBuilder app)
{
var options = new OAuthAuthorizationServerOptions()
{
TokenEndpointPath = new Microsoft.Owin.PathString("/token"),
AccessTokenExpireTimeSpan = TimeSpan.FromHours(1),
AllowInsecureHttp = true,
Provider = new AuthorizationServerProvider()
};
app.UseOAuthAuthorizationServer(options);
app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions());
}
}
Here is the controller
[RoutePrefix("api/v2/game/abc101")]
public class A101Controller : ApiController
{
private readonly IGameServicesABC101 _gameServices;
private readonly IMapper _mapper;
public A101Controller(IGameServicesABC101 gameServices, IMapper mapper)
{
_gameServices = gameServices;
_mapper = mapper;
}
[HttpPost]
[Authorize]
[Route("purchase")]
public async Task<IHttpActionResult> PurchaseGame(RequestDto game)
{
if (!ModelState.IsValid) return BadRequest(ModelState);
...
Basic Authentication Attribute
public class BasicAuthenticationAttribute : AuthorizationFilterAttribute
{
private const string Realm = "My Realm";
private readonly Func<IUserValidate> _factory;
public BasicAuthenticationAttribute(Func<IUserValidate> factory)
{
_factory = factory;
}
public override void OnAuthorization(HttpActionContext actionContext)
{
if (actionContext.Request.Headers.Authorization == null)
{
actionContext.Response = actionContext.Request
.CreateResponse(HttpStatusCode.Unauthorized);
if (actionContext.Response.StatusCode == HttpStatusCode.Unauthorized)
actionContext.Response.Headers.Add("WWW-Authenticate",
$"Basic realm=\"{Realm}\"");
}
else
{
var authenticationToken = actionContext.Request.Headers
.Authorization.Parameter;
try
{
//Decode the string
var decodedAuthenticationToken = Encoding.UTF8.GetString(
Convert.FromBase64String(authenticationToken));
var usernamePasswordArray = decodedAuthenticationToken.Split(':');
var username = usernamePasswordArray[0];
var password = usernamePasswordArray[1];
var uv = _factory();
if (uv.Login(username, password))
{
var identity = new GenericIdentity(username);
IPrincipal principal = new GenericPrincipal(identity, null);
Thread.CurrentPrincipal = principal;
if (HttpContext.Current != null) HttpContext.Current.User = principal;
}
else
{
actionContext.Response = actionContext.Request
.CreateResponse(HttpStatusCode.Unauthorized);
}
}
catch
{
actionContext.Response = actionContext.Request
.CreateResponse(HttpStatusCode.Unauthorized);
}
}
}
}
I am using Unity in my application. Basic Authentication works as expected. When I make a request without a token to ...api/v2/game/abc101/purchase I get a response either. Shouldn't I get 401? What I am missing?
UPDATE
I am searching and trying to find how to use both basic authentication and token-based authentication for different controllers. Here is my status update.
There is no code in the Global.asax
Here is the Owin Startup
public class Startup
{
public void Configuration(IAppBuilder app)
{
var config = GlobalConfiguration.Configuration;
WebApiConfig.Register(config);
app.UseWebApi(config);
Configure(app, config.DependencyResolver);
config.EnsureInitialized();
}
private static void Configure(IAppBuilder app, IDependencyResolver resolver)
{
var options = new OAuthAuthorizationServerOptions()
{
TokenEndpointPath = new Microsoft.Owin.PathString("/token"),
AccessTokenExpireTimeSpan = TimeSpan.FromHours(1),
AllowInsecureHttp = true,
Provider = new AuthorizationServerProvider((IUserValidate)resolver.GetService(typeof(IUserValidate)))
};
app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions());
app.UseOAuthAuthorizationServer(options);
}
}
Here is AuthorizationServerProvider
public class AuthorizationServerProvider : OAuthAuthorizationServerProvider
{
private readonly IUserValidate _userValidate;
public AuthorizationServerProvider(IUserValidate userValidate)
{
_userValidate = userValidate;
}
public override async Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)
{
if (!context.TryGetBasicCredentials(out var clientId, out var clientSecret))
{
context.SetError("Error", "Error...");
}
if (_userValidate.Login(clientId, clientSecret))
{
context.Validated();
}
}
public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
{
context.OwinContext.Response.Headers.Add("Access-Control-Allow-Origin", new[] { "*" });
context.OwinContext.Response.Headers.Add("Access-Control-Allow-Headers", new[] { "Content-Type" });
if (_userValidate.Login(context.UserName, context.Password))
{
var identity = new ClaimsIdentity(context.Options.AuthenticationType);
identity.AddClaim(new Claim("sub", context.UserName));
identity.AddClaim(new Claim("role", "admin"));
context.Validated(identity);
}
else
{
context.SetError("Error", "Error...");
}
}
}
The rest is the same as the previous code samples.
When I call ...api/v2/game/abc101/purchase I am getting 401, it is progress. But when I call http://localhost:52908/token I am getting unsupported_grant_type. I am sending requests via Postman and I am sending a POST requests with content-type x-www-form-urlencoded. Grant-Type is password and username/password is also correct.
When I call another controller http://localhost:52908/api/v2/game/purchase basic authentication does NOT work!
Hope someone can help.
UPDATE 1
Now I am getting the token, one step at a time :) How can I also use Basic authentication for another controller?
Here is Startup
public class Startup
{
public void Configuration(IAppBuilder app)
{
var config = GlobalConfiguration.Configuration;
Configure(app, config.DependencyResolver);
WebApiConfig.Register(config);
app.UseCors(Microsoft.Owin.Cors.CorsOptions.AllowAll);
app.UseWebApi(config);
config.EnsureInitialized();
}
private static void Configure(IAppBuilder app, IDependencyResolver resolver)
{
var options = new OAuthAuthorizationServerOptions()
{
AllowInsecureHttp = true,
TokenEndpointPath = new Microsoft.Owin.PathString("/token"),
AccessTokenExpireTimeSpan = TimeSpan.FromHours(1),
Provider = new AuthorizationServerProvider((IUserValidate)resolver.GetService(typeof(IUserValidate)))
};
app.UseOAuthAuthorizationServer(options);
app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions());
}
}
Here is the Authorization Server Provider
public class AuthorizationServerProvider : OAuthAuthorizationServerProvider
{
private readonly IUserValidate _userValidate;
public AuthorizationServerProvider(IUserValidate userValidate)
{
_userValidate = userValidate;
}
public override async Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)
{
if (!context.TryGetBasicCredentials(out var clientId, out var clientSecret))
{
context.SetError("invalid_grant", "The user name or password is incorrect.");
}
if (_userValidate.Login(clientId, clientSecret))
{
context.Validated();
}
}
public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
{
if (_userValidate.Login(context.UserName, context.Password))
{
var identity = new ClaimsIdentity(context.Options.AuthenticationType);
identity.AddClaim(new Claim("sub", context.UserName));
identity.AddClaim(new Claim("role", "admin"));
context.Validated(identity);
}
else
{
context.SetError("invalid_grant", "The user name or password is incorrect.");
}
}
}
As I mentioned before, I have Basic Authentication Attribute and somehow I have to use it in my other controller.
UPDATE 2
How can I use OverrideAuthentication and my basic authentication attribute?
public class BasicAuthenticationAttribute : AuthorizationFilterAttribute
{
private const string Realm = "My Realm";
private readonly Func<IUserValidate> _factory;
public BasicAuthenticationAttribute(Func<IUserValidate> factory)
{
_factory = factory;
}
...
UPDATE 3
I tried this in my basic authentication attribute OnAuthorization method;
var authentication = DependencyResolver.Current.GetService<IUserValidate>();
if (authentication.Login(username, password))
{
var identity = new GenericIdentity(username);
IPrincipal principal = new GenericPrincipal(identity, null);
Thread.CurrentPrincipal = principal;
if (HttpContext.Current != null) HttpContext.Current.User = principal;
}
There are 2 problems, authentication is null, and somehow authentication token in the attribute is the bearer authentication username/password even though I use basic authentication username/password in the request. It's very weird!
/Get the authentication token from the request header
var authenticationToken = actionContext.Request.Headers
.Authorization.Parameter;
Any help please?
Thanks in advance.
After long googling, here is how I managed to use both basic authentication and bearer authentication for my different controllers.
In Custom basic authentication Attribute I used dependency and requestScope.GetService.
public class CustomBasicAuthenticationAttribute : AuthorizationFilterAttribute
{
[Dependency] public static IUserValidate authentication { get; set; }
private const string Realm = "My Realm";
public override void OnAuthorization(HttpActionContext actionContext)
{
var requestScope = actionContext.Request.GetDependencyScope();
//If the Authorization header is empty or null
//then return Unauthorized
if (actionContext.Request.Headers.Authorization == null)
{
actionContext.Response = actionContext.Request
.CreateResponse(HttpStatusCode.Unauthorized);
// If the request was unauthorized, add the WWW-Authenticate header
// to the response which indicates that it require basic authentication
if (actionContext.Response.StatusCode == HttpStatusCode.Unauthorized)
actionContext.Response.Headers.Add("WWW-Authenticate",
$"Basic realm=\"{Realm}\"");
}
else
{
//Get the authentication token from the request header
var authenticationToken = actionContext.Request.Headers
.Authorization.Parameter;
try
{
//Decode the string
var decodedAuthenticationToken = Encoding.UTF8.GetString(
Convert.FromBase64String(authenticationToken));
//Convert the string into an string array
var usernamePasswordArray = decodedAuthenticationToken.Split(':');
//First element of the array is the username
var username = usernamePasswordArray[0];
//Second element of the array is the password
var password = usernamePasswordArray[1];
authentication = requestScope.GetService(typeof(IUserValidate)) as IUserValidate;
if (authentication != null && authentication.Login(username, password))
{
var identity = new GenericIdentity(username);
IPrincipal principal = new GenericPrincipal(identity, null);
Thread.CurrentPrincipal = principal;
if (HttpContext.Current != null) HttpContext.Current.User = principal;
}
else
{
actionContext.Response = actionContext.Request
.CreateResponse(HttpStatusCode.Unauthorized);
}
}
catch
{
actionContext.Response = actionContext.Request
.CreateResponse(HttpStatusCode.Unauthorized);
}
}
}
}
In one of my controller, I added those attributes
[OverrideAuthentication]
[CustomBasicAuthentication]
[HttpPost, Route("purchase")]
public async Task<IHttpActionResult> PurchaseGame(RequestDto game)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
...
Now I can use bearer token authentication for ...api/v2/game/abc101/purchase and basic authentication for ...api/v2/game/purchase.
Update
The vital part is the dependency and actionContext.Request.GetDependencyScope();. Without OverrideAuthentication it is working as expected.
Hope this solution helps for others.
I have a .net core 3.1 web API ,which was built with JWT authentication and it is integrated with Angular UI and it working as expected.
following is my JWT authentication middleware
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
})
// Adding Jwt Bearer
.AddJwtBearer(options =>
{
options.SaveToken = true;
options.RequireHttpsMetadata = false;
options.IncludeErrorDetails = true;
options.TokenValidationParameters = new TokenValidationParameters()
{
ValidateIssuer = false,
ValidateAudience = false,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["JWT:Secret"]))
};
options.Events = new JwtBearerEvents
{
OnAuthenticationFailed = context =>
{
if (context.Exception.GetType() == typeof(SecurityTokenExpiredException))
{
context.Response.Headers.Add("Token-Expired", "true");
}
return Task.CompletedTask;
}
};
});
Now i need to create some more Web API methods which will be consumed by Angular UI as well as some existing scheduled tasks (.net console applications which will be consuming the web api methods) which are created for internal operations and will be running in the background.
My API controllers are decorated with [Authorize] attribute. It is working fine with Angular UI where the authentication and authorization are implemented using JWT bearer token.The problem is now with the integration of scheduled tasks which does not have logic for getting the tokens.
How to integrate these console apps with .net core web API in terms of authentication? the easiest option (which i thought) is to create a user login with like username "servicetask" and obtain token based on that username and do the API operation (but this requires more effort since no.of console apps are more and there and some apps from other projects also).
Is there any way to handle authentication in this case?
Is it good practice to pass some API key from console application and by pass the authentication in web API ? is that possible ? then how to handle the request in .net core web api?
Is it possible to create any JWT role or claims for these service account and validate them?
Please help.
Best approach would be to allow both bearer token and API key authorization, especially since you are allowing access for users and (internal) services.
Add API key middleware (I personally use this, it's simple to use - package name is AspNetCore.Authentication.ApiKey) with custom validation (store API keys in database along with regular user data or in config, whatever you prefer). Modify [Authorize] attributes on controllers so both Bearer and ApiKey authorization can be used. Angular app continues to use Bearer authentication and any service/console apps (or any other client, including Angular client if needed in some case) sends X-Api-Key header containing API key assigned to that app.
Middleware configuration should look something like this:
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddApiKeyInHeader(ApiKeyDefaults.AuthenticationScheme, options =>
{
options.KeyName = "X-API-Key";
options.SuppressWWWAuthenticateHeader = true;
options.Events = new ApiKeyEvents
{
// A delegate assigned to this property will be invoked just before validating the api key.
OnValidateKey = async (context) =>
{
var apiKey = context.ApiKey.ToLower();
// custom code to handle the api key, create principal and call Success method on context. apiUserService should look up the API key and determine is it valid and which user/service is using it
var apiUser = apiUserService.Validate(apiKey);
if (apiUser != null)
{
... fill out the claims just as you would for user which authenticated using Bearer token...
var claims = GenerateClaims();
context.Principal = new ClaimsPrincipal(new ClaimsIdentity(claims, context.Scheme.Name));
context.Success();
}
else
{
// supplied API key is invalid, this authentication cannot proceed
context.NoResult();
}
}
};
})
// continue with JwtBearer code you have
.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, x => ...
This sorts out Startup.cs part.
Now, in controllers, where you want to enable both Bearer and ApiKey authentication, modify attribute so it looks like this:
[Route("api/[controller]")]
[ApiController]
[Authorize(AuthenticationSchemes = "ApiKey, Bearer")]
public class SomeController : ControllerBase
Now, Angular client will still work in the same way but console app might call API like this:
using (HttpClient client = new HttpClient())
{
// header must match definition in middleware
client.DefaultRequestHeaders.Add("X-API-Key", "someapikey");
client.BaseAddress = new Uri(url);
using (HttpResponseMessage response = await client.PostAsync(url, q))
{
using (HttpContent content =response.Content)
{
string mycontent = await content.ReadAsStringAsync();
}
}
}
This approach in my opinion makes best use of AuthenticationHandler and offers cleanest approach of handling both "regular" clients using JWT and services using fixed API keys, closely following something like OAuth middleware. More details about building custom authentication handler if someone wants to build something like this from scratch, implementing basically any kind of authentication.
Downside of course is security of those API keys even if you are using them for internal services only. This problem can be remedied a bit by limiting access scope for those API keys using Claims, not using same API key for multiple services and changing them periodically. Also, API keys are vulnerable to interception (MITM) if SSL is not used so take care of that.
1.IMO, no , this won't be good idea.
2. Yes you can use claims for this scenario .
Use a BackgroundService to run your task and inject claims principle on this class.
This sample is for service provider account claims:
serviceAccountPrincipleProvider.cs
public class ServiceAccountPrincipalProvider : IClaimsPrincipalProvider
{
private readonly ITokenProvider tokenProvider;
public ServiceAccountPrincipalProvider(ITokenProvider tokenProvider)
{
this.tokenProvider = tokenProvider;
}
public ClaimsPrincipal CurrentPrincipal
{
get
{
var accessToken = tokenProvider.GetAccessTokenAsync().GetAwaiter().GetResult();
if (accessToken == null)
return null;
var identity = new ClaimsIdentity(AuthenticationTypes.Federation);
identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, accessToken.Subject));
identity.AddClaim(new Claim(AppClaimTypes.Issuer, accessToken.Issuer));
identity.AddClaim(new Claim(AppClaimTypes.AccessToken, accessToken.RawData));
return new ClaimsPrincipal(identity);
}
}
}
This is your IClaimsProvider interface:
public interface IClaimsPrincipalProvider
{
ClaimsPrincipal CurrentPrincipal { get; }
}
Don`t bypass authentication. You can pass appKey (key to identify the app instance) to webapi endpoint that is responsible for identifying your dotnet console apps. If appkey is part of your registered appkeys list, let the webapi endpoint get token on behalf of the console app by subsequent authentication step with your webapi auth service and return a JWT token to the console app.
In my case I have console apps running on dotnet 4.5, I mention this because HttpClient is not available in previous versions. With HttpClient you can then do the following in your console app.
HttpClient client = new HttpClient();
client.BaseAddress = new Uri("localhost://mywebapi/");
client.DefaultRequestHeaders.Accept.Clear();
client.DefaultRequestHeaders.Accept.Add(
new MediaTypeWithQualityHeaderValue("application/text"));
HttpResponseMessage response= client.GetAsync("api/appidentityendpoint/appkey").GetAwaiter().GetResult();
var bytarr = response.Content.ReadAsByteArrayAsync().GetAwaiter().GetResult();
string responsemessage = Encoding.UTF8.GetString(bytarr);
res = JsonConvert.DeserializeObject<Authtoken>(responsemessage);
Authtoken object can be as simple as
public class Authtoken
{
public string JwtToken{ get; set; }
}
and once you have your token you add it to your HttpClient headers for subsequent calls to your protected endpoints
client.DefaultRequestHeaders.Add("Authorization", "Bearer " + res.JwtToken);
client.GetAsync("api/protectedendpoint").GetAwaiter().GetResult();
Error handling is obviously required to handle reauthentication in case of token expiry etc
On server side, a simplified example is as follows
[Produces("application/json")]
[Route("api/Auth")]
public class AuthController : Controller
{
private readonly IAppRegService _regAppService;
public AuthController(IAppRegService regAppService){
_regAppService = regAppService;
};
//api/auth/console/login/585
[HttpGet, Route("console/login/{appkey}")]
public async Task<IActionResult> Login(string appkey)
{
// write logic to check in your db if appkey is the key of a registered console app.
// _regAppService has methods to connect to db or read file to check if key exists from your repository of choice
var appkeyexists = _regAppService.CheckAppByAppKey(appkey);
if(appkeyexists){
//create claims list
List<Claim> claims = new List<Claim>();
claims.Add(new Claim("appname", "console",ClaimValueTypes.String));
claims.Add(new Claim("role","daemon",ClaimValueTypes.String));
//create a signing secret
var secretKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("yoursecretkey"));
var signinCredentials = new SigningCredentials(secretKey, SecurityAlgorithms.HmacSha256);
//create token options
var tokenOptions = new JwtSecurityToken(
issuer: "serverurl",
audience:"consoleappname",
claims: claims,
expires: DateTime.Now.AddDays(5),
signingCredentials: signinCredentials
);
//create token
var tokenString = new JwtSecurityTokenHandler().WriteToken(tokenOptions);
//return token
return new OkObjectResult(new Authtoken { JwtToken= tokenString });
} else {
return Unauthorized();
}
}
}
I will present how I do JWT Auth from a WebAssembly app to a .NET Core API. Everything is based on this YouTube video. It explains everything you need to know. Down below is a sample of code from the video to give you an idea of what you have to do.
This is my Auth Controller:
// A bunch of usings
namespace Server.Controllers.Authentication
{
[Authorize]
[ApiController]
[Route("api/[controller]")]
public class AuthenticateController : ControllerBase
{
private readonly UserManager<ApplicationUser> userManager;
private readonly RoleManager<IdentityRole> roleManager;
private readonly IConfiguration _configuration;
private readonly AppContext appContext;
public AuthenticateController(UserManager<ApplicationUser> userManager, RoleManager<IdentityRole> roleManager, IConfiguration configuration, AppContext appContext)
{
this.userManager = userManager;
this.roleManager = roleManager;
this._configuration = configuration;
this.appContext = appContext;
}
[HttpPost]
[Route("login")]
[AllowAnonymous]
public async Task<IActionResult> Login([FromBody] LoginModel loginModel)
{
ApplicationUser user = await userManager.FindByNameAsync(loginModel.Username);
if ((user is not null) && await userManager.CheckPasswordAsync(user, loginModel.Password))
{
IList<string> userRoles = await userManager.GetRolesAsync(user);
List<Claim> authClaims = new()
{
new Claim(ClaimTypes.Name, user.UserName),
new Claim(ClaimTypes.NameIdentifier, user.Id),
new Claim(Microsoft.IdentityModel.JsonWebTokens.JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
new Claim(ClaimTypes.AuthenticationMethod, "pwd")
};
foreach (string role in userRoles)
{
authClaims.Add(new Claim(ClaimTypes.Role, role));
}
SymmetricSecurityKey authSigningKey = new(Encoding.UTF8.GetBytes(_configuration["JWT:Secret"]));
//SymmetricSecurityKey authSigningKey = Startup.SecurityAppKey;
JwtSecurityToken token = new(
issuer: _configuration["JWT:ValidIssuer"],
//audience: _configuration["JWT:ValidAudience"],
expires: DateTime.Now.AddHours(3),
claims: authClaims,
signingCredentials: new SigningCredentials(authSigningKey, SecurityAlgorithms.HmacSha256)
);
return Ok(new
{
token = new JwtSecurityTokenHandler().WriteToken(token),
expiration = token.ValidTo
});
}
return Unauthorized();
}
[HttpPost]
[Route("register")]
[AllowAnonymous]
public async Task<IActionResult> Register([FromBody] RegisterModel model)
{
ApplicationUser userExists = await userManager.FindByNameAsync(model.Username);
if (userExists != null)
{
return StatusCode(StatusCodes.Status500InternalServerError, new Response { Status = "Error", Message = "User already exists!" });
}
ApplicationUser user = new()
{
Email = model.Email,
SecurityStamp = Guid.NewGuid().ToString(),
UserName = model.Username
};
IdentityResult result = await userManager.CreateAsync(user, model.Password);
if (!result.Succeeded)
{
return StatusCode(StatusCodes.Status500InternalServerError, new Response { Status = "Error", Message = "User creation failed! Please check user details and try again." });
}
await userManager.AddToRoleAsync(user, UserRoles.User);
return Ok(new Response { Status = "Success", Message = "User created successfully!" });
}
}
}
When a user registers it is automatically added to the User Role. What you should do is to create accounts for each of your console apps, or even a global account for all internal apps, and then assign it to a custom role.
After that, on all API endpoints that are only accessible by your internal apps add this attribute: [Authorize(Roles = UserRoles.Internal)]
UserRoles is a static class that has string properties for each role.
More info about Role-based authorization can be found here.
You can create a login password configuration on appsettings, db or somewhere to send a token (web api).
Worker.cs (console app)
public struct UserLogin
{
public string user;
public string password;
}
// ...
private async Task<string> GetToken(UserLogin login)
{
try {
string token;
var content = new StringContent(JsonConvert.SerializeObject(login), Encoding.UTF8, "application/json");
using (var httpClient = new HttpClient())
using (var response = await httpClient.PostAsync($"{api}/login", content))
{
var result = await response.Content.ReadAsStringAsync();
var request = JsonConvert.DeserializeObject<JObject>(result);
token = request["token"].ToObject<string>();
}
return token;
}
catch (Exception e)
{
throw new Exception(e.Message);
}
}
Give your console a jwt token without an expiration date or one that gives you enough time. If you need to invalidate the token follow this link. Add the jwt on appsettings.json and read the token as follows:
appsettings.json
{
//...
"Worker" : "dotnet",
"Token": "eyJhbGciOiJIUzI1Ni...",
"ApiUrl": "http://localhost:3005",
//...
}
Worker.cs
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Configuration;
public Worker(ILogger<Worker> _logger, IConfiguration _cfg)
{
logger = _logger;
//...
api = _cfg["ApiUrl"];
token = _cfg["Token"];
}
private async Task SendResult(SomeModel JobResult)
{
var content = new StringContent(JsonConvert.SerializeObject(JobResult), Encoding.UTF8, "application/json");
using (var httpClient = new HttpClient())
{
httpClient.DefaultRequestHeaders.Add("Authorization", "Bearer " + token);
using (var response = await httpClient.PostAsync($"{api}/someController", content))
{
var result = await response.Content.ReadAsStringAsync();
var rs = JsonConvert.DeserializeObject(result);
Console.WriteLine($"API response {response.ReasonPhrase}");
}
}
}
Update:
If you need to control requests:
Startup.cs
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters()
{
// ...
};
options.Events = new JwtBearerEvents
{
OnTokenValidated = TokenValidation
};
});
private static Task TokenValidation(TokenValidatedContext context)
{
// your custom validation
var hash = someHashOfContext();
if (context.Principal.FindFirst(ClaimTypes.Hash).Value != hash)
{
context.Fail("You cannot access here");
}
return Task.CompletedTask;
}
I have a Web API project using OAuth/Owin to deal with authentication.
Everything is working fine when i post through form-urlencoded. But the frontend team will post application/json, and i was not able to change my method to receive this Json.
I usually use [FromBody] when i want to recieve a Json, but it didn't work this time.
My code:
public override async Task ValidateClientAuthentication([FromBody]OAuthValidateClientAuthenticationContext context)
{
context.Validated();
}
public override async Task GrantResourceOwnerCredentials([FromBody]OAuthGrantResourceOwnerCredentialsContext context)
{
try
{
....
}
catch (Exception e)
{
context.SetError("invalid_grant", "User not found");
}
}
}
My OAuth Config:
public static void ConfigureOAuth(IAppBuilder app, IContainer container)
{
OAuthAuthorizationServerOptions OAuthServerOptions = new OAuthAuthorizationServerOptions()
{
AllowInsecureHttp = true, // HTTPS == false
TokenEndpointPath = new PathString("/security/login"),
AccessTokenExpireTimeSpan = TimeSpan.FromHours(2),
Provider = container.Resolve<IOAuthAuthorizationServerProvider>()
};
app.UseOAuthAuthorizationServer(OAuthServerOptions);
app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions());
}
Json Example:
{grant_type: "password", username : "myuser", password: "mypass"}
Read in the request body by
context.Request.Body.Position = 0; // this resets the read position to 0
var payload = await new StreamReader(context.Request.Body).ReadToEndAsync();
From here you have the string of your JSON object. You can use a deserializer to convert it to a CLR type.
I apologize in advance for asking this as I have next to no knowledge of security in general and IdentityServer in particular.
I am trying to set up IdentityServer to manage security for an Asp.Net MVC application.
I am following the tutorial on their website: Asp.Net MVC with IdentityServer
However, I am doing something slightly different in that I have a separate project for the Identity "Server" part, which leads to 2 Startup.cs files, one for the application and one for the Identity Server
For the application, the Startup.cs file looks like this
public class Startup
{
public void Configuration(IAppBuilder app)
{
AntiForgeryConfig.UniqueClaimTypeIdentifier = Constants.ClaimTypes.Subject;
JwtSecurityTokenHandler.InboundClaimTypeMap = new Dictionary<string, string>();
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = "Cookies"
});
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
Authority = "https://localhost:44301/identity",
ClientId = "baseballStats",
Scope = "openid profile roles baseballStatsApi",
RedirectUri = "https://localhost:44300/",
ResponseType = "id_token token",
SignInAsAuthenticationType = "Cookies",
UseTokenLifetime = false,
Notifications = new OpenIdConnectAuthenticationNotifications
{
SecurityTokenValidated = async n =>
{
var userInfoClient = new UserInfoClient(
new Uri(n.Options.Authority + "/connect/userinfo"),
n.ProtocolMessage.AccessToken);
var userInfo = await userInfoClient.GetAsync();
// create new identity and set name and role claim type
var nid = new ClaimsIdentity(
n.AuthenticationTicket.Identity.AuthenticationType,
Constants.ClaimTypes.GivenName,
Constants.ClaimTypes.Role);
userInfo.Claims.ToList().ForEach(c => nid.AddClaim(new Claim(c.Item1, c.Item2)));
// keep the id_token for logout
nid.AddClaim(new Claim("id_token", n.ProtocolMessage.IdToken));
// add access token for sample API
nid.AddClaim(new Claim("access_token", n.ProtocolMessage.AccessToken));
// keep track of access token expiration
nid.AddClaim(new Claim("expires_at", DateTimeOffset.Now.AddSeconds(int.Parse(n.ProtocolMessage.ExpiresIn)).ToString()));
// add some other app specific claim
nid.AddClaim(new Claim("app_specific", "some data"));
n.AuthenticationTicket = new AuthenticationTicket(
nid,
n.AuthenticationTicket.Properties);
}
}
});
app.UseResourceAuthorization(new AuthorizationManager());
app.UseIdentityServerBearerTokenAuthentication(new IdentityServerBearerTokenAuthenticationOptions
{
Authority = "https://localhost:44301/identity",
RequiredScopes = new[] { "baseballStatsApi"}
});
var config = new HttpConfiguration();
config.MapHttpAttributeRoutes();
app.UseWebApi(config);
}
}
For the identity server, the startup.cs file is
public class Startup
{
public void Configuration(IAppBuilder app)
{
app.Map("/identity", idsrvApp =>
{
idsrvApp.UseIdentityServer(new IdentityServerOptions
{
SiteName = "Embedded IdentityServer",
SigningCertificate = LoadCertificate(),
Factory = InMemoryFactory.Create(
users: Users.Get(),
clients: Clients.Get(),
scopes: Scopes.Get())
});
});
}
X509Certificate2 LoadCertificate()
{
return new X509Certificate2(
string.Format(#"{0}\bin\Configuration\idsrv3test.pfx", AppDomain.CurrentDomain.BaseDirectory), "idsrv3test");
}
}
I am also setting up an Authorization Manager
public class AuthorizationManager : ResourceAuthorizationManager
{
public override Task<bool> CheckAccessAsync(ResourceAuthorizationContext context)
{
switch (context.Resource.First().Value)
{
case "Players":
return CheckAuthorization(context);
case "About":
return CheckAuthorization(context);
default:
return Nok();
}
}
private Task<bool> CheckAuthorization(ResourceAuthorizationContext context)
{
switch(context.Action.First().Value)
{
case "Read":
return Eval(context.Principal.HasClaim("role", "LevelOneSubscriber"));
default:
return Nok();
}
}
}
So for instance, if I define a controller method that is decorated with the ResourceAuthorize attribute, like so
public class HomeController : Controller
{
[ResourceAuthorize("Read", "About")]
public ActionResult About()
{
return View((User as ClaimsPrincipal).Claims);
}
}
Then, when I first try to access this method, I will be redirected to the default login page.
What I don't understand however, is why when I login with the user I have defined for the application (see below),
public class Users
{
public static List<InMemoryUser> Get()
{
return new List<InMemoryUser>
{
new InMemoryUser
{
Username = "bob",
Password = "secret",
Subject = "1",
Claims = new[]
{
new Claim(Constants.ClaimTypes.GivenName, "Bob"),
new Claim(Constants.ClaimTypes.FamilyName, "Smith"),
new Claim(Constants.ClaimTypes.Role, "Geek"),
new Claim(Constants.ClaimTypes.Role, "LevelOneSubscriber")
}
}
};
}
}
I get a 403 error, Bearer error="insufficient_scope".
Can anybody explain what I am doing wrong?
Any subsequent attempt to access the action method will return the same error. It seems to me that the user I defined has the correct claims to be able to access this method. However, the claims check only happens once, when I first try to access this method. After I login I get a cookie, and the claims check is not made during subsequent attempts to access the method.
I'm a bit lost, and would appreciate some help in clearing this up.
Thanks in advance.
EDIT: here are the scoles and client classes
public static class Scopes
{
public static IEnumerable<Scope> Get()
{
var scopes = new List<Scope>
{
new Scope
{
Enabled = true,
Name = "roles",
Type = ScopeType.Identity,
Claims = new List<ScopeClaim>
{
new ScopeClaim("role")
}
},
new Scope
{
Enabled = true,
Name = "baseballStatsApi",
Description = "Access to baseball stats API",
Type = ScopeType.Resource,
Claims = new List<ScopeClaim>
{
new ScopeClaim("role")
}
}
};
scopes.AddRange(StandardScopes.All);
return scopes;
}
}
And the Client class
public static class Clients
{
public static IEnumerable<Client> Get()
{
return new[]
{
new Client
{
Enabled = true,
ClientName = "Baseball Stats Emporium",
ClientId = "baseballStats",
Flow = Flows.Implicit,
RedirectUris = new List<string>
{
"https://localhost:44300/"
}
},
new Client
{
Enabled = true,
ClientName = "Baseball Stats API Client",
ClientId = "baseballStats_Api",
ClientSecrets = new List<ClientSecret>
{
new ClientSecret("secret".Sha256())
},
Flow = Flows.ClientCredentials
}
};
}
}
I have also created a custom filter attribute which I use to determine when the claims check is made.
public class CustomFilterAttribute : ResourceAuthorizeAttribute
{
public CustomFilterAttribute(string action, params string[] resources) : base(action, resources)
{
}
protected override bool CheckAccess(HttpContextBase httpContext, string action, params string[] resources)
{
return base.CheckAccess(httpContext, action, resources);
}
}
The breakpoint is hit only on the initial request to the url. On subsequent requests, the filter attribute breakpoint is not hit, and thus no check occurs. This is surprising to me as I assumed the check would have to be made everytime the url is requested.
You need to request the scopes required by the api when the user logs in.
Scope = "openid profile roles baseballStatsApi"
Authority = "https://localhost:44301/identity",
ClientId = "baseballStats",
Scope = "openid profile roles baseballStatsApi",
ResponseType = "id_token token",
RedirectUri = "https://localhost:44300/",
SignInAsAuthenticationType = "Cookies",
UseTokenLifetime = false,