OpenIdConnect Redirect on Form POST - asp.net-core

Why does a form POST with an expired access_token result in a GET when using the Microsoft.AspNetCore.Authentication.OpenIdConnect middleware? When this happens, any data entered into a form is lost, since it doesn't reach the HttpPost endpoint. Instead, the request is redirected to the same URI with a GET, following the signin-oidc redirect. Is this a limitation, or do I have something configured incorrectly?
I noticed this issue after shortening the AccessTokenLifetime with the intent of forcing the user's claims to be renewed more frequently (i.e. if the user were disabled or they had claims revoked). I have only reproduced this when the OpenIdConnect middleware's OpenIdConnectionOptions are set to true options.UseTokenLifetime = true; (setting this to false results in the authenticated user's claims not being updated as expected).
I was able to recreate and demonstrate this behavior using the IdentityServer4 sample quickstart 5_HybridFlowAuthenticationWithApiAccess with the following changes below. Basically, there is an authorized form that has an HttpGet and an HttpPost method. If you wait longer than the AccessTokenLifetime (configured to only 30 seconds in this example) to submit the form, the HttpGet method is called instead of the HttpPost method.
Modifications to MvcClient/Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
services.AddAuthentication(options =>
{
options.DefaultScheme = "Cookies";
options.DefaultChallengeScheme = "oidc";
})
.AddCookie("Cookies", options =>
{
// the following was added
options.SlidingExpiration = false;
})
.AddOpenIdConnect("oidc", options =>
{
options.SignInScheme = "Cookies";
options.Authority = "http://localhost:5000";
options.RequireHttpsMetadata = false;
options.ClientId = "mvc";
options.ClientSecret = "secret";
options.ResponseType = "code id_token";
options.SaveTokens = true;
options.GetClaimsFromUserInfoEndpoint = true;
options.Scope.Add("openid");
options.Scope.Add("api1");
options.ClaimActions.MapJsonKey("website", "website");
// the following were changed
options.UseTokenLifetime = true;
options.Scope.Add("offline_access");
});
}
Modifications to the Client list in IdentityServer/Config.cs
new Client
{
ClientId = "mvc",
ClientName = "MVC Client",
AllowedGrantTypes = GrantTypes.Hybrid,
ClientSecrets =
{
new Secret("secret".Sha256())
},
RedirectUris = { "http://localhost:5002/signin-oidc" },
PostLogoutRedirectUris = { "http://localhost:5002/signout-callback-oidc" },
AllowedScopes =
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
"api1",
IdentityServerConstants.StandardScopes.OfflineAccess,
},
AllowOfflineAccess = true,
// the following properties were configured:
AbsoluteRefreshTokenLifetime = 14*60*60,
AccessTokenLifetime = 30,
IdentityTokenLifetime = 15,
AuthorizationCodeLifetime = 15,
SlidingRefreshTokenLifetime = 60,
RefreshTokenUsage = TokenUsage.OneTimeOnly,
UpdateAccessTokenClaimsOnRefresh = true,
RequireConsent = false,
}
Added to MvcClient/Controllers/HomeController
[Authorize]
[HttpGet]
[Route("home/test", Name = "TestRouteGet")]
public async Task<IActionResult> Test()
{
TestViewModel viewModel = new TestViewModel
{
Message = "GET at " + DateTime.Now,
TestData = DateTime.Now.ToString(),
AccessToken = await this.HttpContext.GetTokenAsync("access_token"),
RefreshToken = await this.HttpContext.GetTokenAsync("refresh_token"),
};
return View("Test", viewModel);
}
[Authorize]
[HttpPost]
[Route("home/test", Name = "TestRoutePost")]
public async Task<IActionResult> Test(TestViewModel viewModel)
{
viewModel.Message = "POST at " + DateTime.Now;
viewModel.AccessToken = await this.HttpContext.GetTokenAsync("access_token");
viewModel.RefreshToken = await this.HttpContext.GetTokenAsync("refresh_token");
return View("Test", viewModel);
}

After further research and investigation, I came to the conclusion that completing a form POST that is redirected to the OIDC provider is not supported out of the box (at least for Identity Server, but I suspect that is also true for other identity connect providers as well). Here is the only mention I can find of this: Sending Custom Parameters to Login Page
I was able to come up with a workaround for the issue, which I've outlined below and is hopefully useful to others. The key components are the following OpenIdConnect and Cookie middleware events:
OpenIdConnectEvents.OnRedirectToIdentityProvider - save Post requests for later retrieval
CookieAuthenticationEvents.OnValidatePrincipal - check for saved Post requests and update the current request with saved state
The OpenIdConnect middleware exposes the OnRedirectToIdentityProvider event which gives us the opportunity to:
determine if this is a form post for an expired access token
modify the RedirectContext to include a custom request id with the AuthenticationProperties Items dictionary
Map the current HttpRequest to an HttpRequestLite object that can be persisted to a cache store, I recommend using an expiring, distributed cache for load-balanced environments. I'm using a static dictionary here for simplicity
new OpenIdConnectEvents
{
OnRedirectToIdentityProvider = async (context) =>
{
if (context.HttpContext.Request.Method == HttpMethods.Post && context.Properties.ExpiresUtc == null)
{
string requestId = Guid.NewGuid().ToString();
context.Properties.Items["OidcPostRedirectRequestId"] = requestId;
HttpRequest requestToSave = context.HttpContext.Request;
// EXAMPLE - saving this to memory which would work on a non-loadbalanced or stateful environment. Recommend persisting to external store such as Redis.
postedRequests[requestId] = await HttpRequestLite.BuildHttpRequestLite(requestToSave);
}
return;
},
};
The Cookie middleware exposes the OnValidatePrincipal event which gives us the opportunity to:
check the CookieValidatePrincipalContext for AuthenticationProperties Items for custom dictionary items. We check it for the ID of our saved/cached request
it's important that we remove the item after we read it so that subsequent requests do not replay the wrong form submission, setting ShouldRenew to true persists any changes on subsequent requests
check our external cache for items that match our key, I recommend using an expiring, distributed cache for load-balanced environments. I'm using a static dictionary here for simplicity
read our custom HttpRequestLite object and override the Request object in the CookieValidatePrincipalContext object
new CookieAuthenticationEvents
{
OnValidatePrincipal = (context) =>
{
if (context.Properties.Items.ContainsKey("OidcPostRedirectRequestId"))
{
string requestId = context.Properties.Items["OidcPostRedirectRequestId"];
context.Properties.Items.Remove("OidcPostRedirectRequestId");
context.ShouldRenew = true;
if (postedRequests.ContainsKey(requestId))
{
HttpRequestLite requestLite = postedRequests[requestId];
postedRequests.Remove(requestId);
if (requestLite.Body?.Any() == true)
{
context.Request.Body = new MemoryStream(requestLite.Body);
}
context.Request.ContentLength = requestLite.ContentLength;
context.Request.ContentLength = requestLite.ContentLength;
context.Request.ContentType = requestLite.ContentType;
context.Request.Method = requestLite.Method;
context.Request.Headers.Clear();
foreach (var header in requestLite.Headers)
{
context.Request.Headers.Add(header);
}
}
}
return Task.CompletedTask;
},
};
We need a class to map HttpRequest to/from for serialization purposes. This reads the HttpRequest and it's body without modifying the contents, it leaves the HttpRequest untouched for additional middleware that may try to read it after we do (this is important when trying to read the Body stream which can only be read once by default).
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Internal;
using Microsoft.Extensions.Primitives;
public class HttpRequestLite
{
public static async Task<HttpRequestLite> BuildHttpRequestLite(HttpRequest request)
{
HttpRequestLite requestLite = new HttpRequestLite();
try
{
request.EnableRewind();
using (var reader = new StreamReader(request.Body))
{
string body = await reader.ReadToEndAsync();
request.Body.Seek(0, SeekOrigin.Begin);
requestLite.Body = Encoding.ASCII.GetBytes(body);
}
//requestLite.Form = request.Form;
}
catch
{
}
requestLite.Cookies = request.Cookies;
requestLite.ContentLength = request.ContentLength;
requestLite.ContentType = request.ContentType;
foreach (var header in request.Headers)
{
requestLite.Headers.Add(header);
}
requestLite.Host = request.Host;
requestLite.IsHttps = request.IsHttps;
requestLite.Method = request.Method;
requestLite.Path = request.Path;
requestLite.PathBase = request.PathBase;
requestLite.Query = request.Query;
requestLite.QueryString = request.QueryString;
requestLite.Scheme = request.Scheme;
return requestLite;
}
public QueryString QueryString { get; set; }
public byte[] Body { get; set; }
public string ContentType { get; set; }
public long? ContentLength { get; set; }
public IRequestCookieCollection Cookies { get; set; }
public IHeaderDictionary Headers { get; } = new HeaderDictionary();
public IQueryCollection Query { get; set; }
public IFormCollection Form { get; set; }
public PathString Path { get; set; }
public PathString PathBase { get; set; }
public HostString Host { get; set; }
public bool IsHttps { get; set; }
public string Scheme { get; set; }
public string Method { get; set; }
}

Related

Azure Devops Oauth authentication: Cannot get access token (BadRequest Failed to deserialize the JsonWebToken object)

I'm trying to implement an OAUth 2.0 flow for custom webapplication for Azure Devops. I'm following this https://learn.microsoft.com/en-us/azure/devops/integrate/get-started/authentication/oauth?view=azure-devops documentation as well as this https://github.com/microsoft/azure-devops-auth-samples/tree/master/OAuthWebSample OauthWebSample but using ASP.NET Core (I also read one issue on SO that looked similar but is not: Access Azure DevOps REST API with oAuth)
Reproduction
I have registered an azdo app at https://app.vsaex.visualstudio.com/app/register and the authorize step seems to work fine, i.e. the user can authorize the app and the redirect to my app returns something that looks like a valid jwt token:
header: {
"typ": "JWT",
"alg": "RS256",
"x5t": "oOvcz5M_7p-HjIKlFXz93u_V0Zo"
}
payload: {
"aui": "b3426a71-1c05-497c-ab76-259161dbcb9e",
"nameid": "7e8ce1ba-1e70-4c21-9b51-35f91deb6d14",
"scp": "vso.identity vso.work_write vso.authorization_grant",
"iss": "app.vstoken.visualstudio.com",
"aud": "app.vstoken.visualstudio.com",
"nbf": 1587294992,
"exp": 1587295892
}
The next step is to get an access token which fails with a BadReqest: invalid_client, Failed to deserialize the JsonWebToken object.
Here is the full example:
public class Config
{
public string ClientId { get; set; } = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx";
public string Secret { get; set; } = "....";
public string Scope { get; set; } = "vso.identity vso.work_write";
public string RedirectUri { get; set; } = "https://....ngrok.io/azdoaccount/callback";
}
/// <summary>
/// Create azdo application at https://app.vsaex.visualstudio.com/
/// Use configured values in above 'Config' (using ngrok to have a public url that proxies to localhost)
/// navigating to localhost:5001/azdoaccount/signin
/// => redirect to https://app.vssps.visualstudio.com/oauth2/authorize and let user authorize (seems to work)
/// => redirect back to localhost:5001/azdoaccount/callback with auth code
/// => post to https://app.vssps.visualstudio.com/oauth2/token => BadReqest: invalid_client, Failed to deserialize the JsonWebToken object
/// </summary>
[Route("[controller]/[action]")]
public class AzdoAccountController : Controller
{
private readonly Config config = new Config();
[HttpGet]
public ActionResult SignIn()
{
Guid state = Guid.NewGuid();
UriBuilder uriBuilder = new UriBuilder("https://app.vssps.visualstudio.com/oauth2/authorize");
NameValueCollection queryParams = HttpUtility.ParseQueryString(uriBuilder.Query ?? string.Empty);
queryParams["client_id"] = config.ClientId;
queryParams["response_type"] = "Assertion";
queryParams["state"] = state.ToString();
queryParams["scope"] = config.Scope;
queryParams["redirect_uri"] = config.RedirectUri;
uriBuilder.Query = queryParams.ToString();
return Redirect(uriBuilder.ToString());
}
[HttpGet]
public async Task<ActionResult> Callback(string code, Guid state)
{
string token = await GetAccessToken(code, state);
return Ok();
}
public async Task<string> GetAccessToken(string code, Guid state)
{
Dictionary<string, string> form = new Dictionary<string, string>()
{
{ "client_assertion_type", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer" },
{ "client_assertion", config.Secret },
{ "grant_type", "urn:ietf:params:oauth:grant-type:jwt-bearer" },
{ "assertion", code },
{ "redirect_uri", config.RedirectUri }
};
HttpClient httpClient = new HttpClient();
HttpResponseMessage responseMessage = await httpClient.PostAsync(
"https://app.vssps.visualstudio.com/oauth2/token",
new FormUrlEncodedContent(form)
);
if (responseMessage.IsSuccessStatusCode) // is always false for me
{
string body = await responseMessage.Content.ReadAsStringAsync();
// TODO parse body and return access token
return "";
}
else
{
// Bad Request ({"Error":"invalid_client","ErrorDescription":"Failed to deserialize the JsonWebToken object."})
string content = await responseMessage.Content.ReadAsStringAsync();
throw new Exception($"{responseMessage.ReasonPhrase} {(string.IsNullOrEmpty(content) ? "" : $"({content})")}");
}
}
}
When asking for access tokens the Client Secret and not the App Secret must be provided for the client_assertion parameter:

Razor Pages .NET Core 2.1 Integration Testing post authentication

I am looking for some guidance...
I'm currently looking at trying to write some integration tests for a Razor Pages app in .net core 2.1, the pages I'm wanting to test are post authentication but I'm not sure about the best way of approaching it. The docs seem to suggest creating a CustomWebApplicationFactory, but apart from that I've got a little bit lost as how I can fake/mock an authenticated user/request, using basic cookie based authentication.
I've seen that there is an open GitHub issue against the Microsoft docs (here is the actual GitHub issue), there was a mentioned solution using IdentityServer4 but I’m just looking how to do this just using cookie based authentication.
Has anyone got any guidance they may be able to suggest?
Thanks in advance
My Code so far is:
Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.Configure<CookiePolicyOptions>(options =>
{
services.AddDbContext<ApplicationDbContext>(options =>
{
options.UseMySql(connectionString);
options.EnableSensitiveDataLogging();
});
services.AddLogging(builder =>
{
builder.AddSeq();
});
services.ConfigureAuthentication();
services.ConfigureRouting();
}
}
ConfigureAuthentication.cs
namespace MyCarparks.Configuration.Startup
{
public static partial class ConfigurationExtensions
{
public static IServiceCollection ConfigureAuthentication(this IServiceCollection services)
{
services.AddIdentity<MyCarparksUser, IdentityRole>(cfg =>
{
//cfg.SignIn.RequireConfirmedEmail = true;
})
.AddDefaultUI()
.AddDefaultTokenProviders()
.AddEntityFrameworkStores<ApplicationDbContext>();
services.ConfigureApplicationCookie(options =>
{
options.LoginPath = $"/Identity/Account/Login";
options.LogoutPath = $"/Identity/Account/Logout";
options.AccessDeniedPath = $"/Identity/Account/AccessDenied";
});
services.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1)
.AddRazorPagesOptions(options =>
{
options.AllowAreas = true;
options.Conventions.AuthorizeAreaFolder("Identity", "/Account/Manage");
options.Conventions.AuthorizeAreaPage("Identity", "/Account/Logout");
options.Conventions.AuthorizeFolder("/Sites");
});
return services;
}
}
}
Integration Tests
PageTests.cs
namespace MyCarparks.Web.IntegrationTests
{
public class PageTests : IClassFixture<CustomWebApplicationFactory<Startup>>
{
private readonly CustomWebApplicationFactory<Startup> factory;
public PageTests(CustomWebApplicationFactory<Startup> webApplicationFactory)
{
factory = webApplicationFactory;
}
[Fact]
public async Task SitesReturnsSuccessAndCorrectContentTypeAndSummary()
{
var siteId = Guid.NewGuid();
var site = new Site { Id = siteId, Address = "Test site address" };
var mockSite = new Mock<ISitesRepository>();
mockSite.Setup(s => s.GetSiteById(It.IsAny<Guid>())).ReturnsAsync(site);
// Arrange
var client = factory.CreateClient();
// Act
var response = await client.GetAsync("http://localhost:44318/sites/sitedetails?siteId=" + siteId);
// Assert
response.EnsureSuccessStatusCode();
response.Content.Headers.ContentType.ToString()
.Should().Be("text/html; charset=utf-8");
var responseString = await response.Content.ReadAsStringAsync();
responseString.Should().Contain("Site Details - MyCarparks");
}
public class CustomWebApplicationFactory<TStartup> : WebApplicationFactory<Startup>
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.UseStartup<Startup>();
}
}
}
For implement your requirement, you could try code below which creates the client with the authentication cookies.
public class CustomWebApplicationFactory<TEntryPoint> : WebApplicationFactory<TEntryPoint> where TEntryPoint : class
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureServices(services =>
{
});
base.ConfigureWebHost(builder);
}
public new HttpClient CreateClient()
{
var cookieContainer = new CookieContainer();
var uri = new Uri("https://localhost:44344/Identity/Account/Login");
var httpClientHandler = new HttpClientHandler
{
CookieContainer = cookieContainer
};
HttpClient httpClient = new HttpClient(httpClientHandler);
var verificationToken = GetVerificationToken(httpClient, "https://localhost:44344/Identity/Account/Login");
var contentToSend = new FormUrlEncodedContent(new[]
{
new KeyValuePair<string, string>("Email", "test#outlook.com"),
new KeyValuePair<string, string>("Password", "1qaz#WSX"),
new KeyValuePair<string, string>("__RequestVerificationToken", verificationToken),
});
var response = httpClient.PostAsync("https://localhost:44344/Identity/Account/Login", contentToSend).Result;
var cookies = cookieContainer.GetCookies(new Uri("https://localhost:44344/Identity/Account/Login"));
cookieContainer.Add(cookies);
var client = new HttpClient(httpClientHandler);
return client;
}
private string GetVerificationToken(HttpClient client, string url)
{
HttpResponseMessage response = client.GetAsync(url).Result;
var verificationToken =response.Content.ReadAsStringAsync().Result;
if (verificationToken != null && verificationToken.Length > 0)
{
verificationToken = verificationToken.Substring(verificationToken.IndexOf("__RequestVerificationToken"));
verificationToken = verificationToken.Substring(verificationToken.IndexOf("value=\"") + 7);
verificationToken = verificationToken.Substring(0, verificationToken.IndexOf("\""));
}
return verificationToken;
}
}
Following Chris Pratt suggestion, but for Razor Pages .NET Core 3.1 I use a previous request to authenticate against login endpoint (which is also another razor page), and grab the cookie from the response. Then I add the same cookie as part of the http request, and voila, it's an authenticated request.
This is a piece of code that uses an HttpClient and AngleSharp, as the official microsoft documentation, to test a razor page. So I reuse it to grab the cookie from the response.
private async Task<string> GetAuthenticationCookie()
{
var formName = nameof(LoginModel.LoginForm); //this is the bounded model for the login page
var dto =
new Dictionary<string, string>
{
[$"{formName}.Username"] = "foo",
[$"{formName}.Password"] = "bar",
};
var page = HttpClient.GetAsync("/login").GetAwaiter().GetResult();
var content = HtmlHelpers.GetDocumentAsync(page).GetAwaiter().GetResult();
//this is the AndleSharp
var authResult =
await HttpClient
.SendAsync(
(IHtmlFormElement)content.QuerySelector("form[id='login-form']"),
(IHtmlButtonElement)content.QuerySelector("form[id='login-form']")
.QuerySelector("button"),
dto);
_ = authResult.Headers.TryGetValues("Set-Cookie", out var values);
return values.First();
}
Then that value can be reused and passed with the new http request.
//in my test, the cookie is a given (from the given-when-then approach) pre-requirement
protected void Given()
{
var cookie = GetAuthenticationCookie().GetAwaiter().GetResult();
//The http client is a property that comes from the TestServer, when creating a client http for tests as usual. Only this time I set the auth cookie to it
HttpClient.DefaultRequestHeaders.Add("Set-Cookie", cookie);
var page = await HttpClient.GetAsync($"/admin/protectedPage");
//this will be a 200 OK because it's an authenticated request with whatever claims and identity the /login page applied
}

Why [Authorize] attribute return 401 status code JWT + Asp.net Web Api?

I'm having big trouble finding issue with the JWT token authentication with asp.net web api. This is first time I am dealing with JWT & Web Api authentication & Authorization.
I have implemented the following code.
Startup.cs
public class Startup
{
public void Configuration(IAppBuilder app)
{
// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=316888
ConfigureOAuthTokenGeneration(app);
ConfigureOAuthTokenConsumption(app);
}
private void ConfigureOAuthTokenGeneration(IAppBuilder app)
{
OAuthAuthorizationServerOptions OAuthServerOptions = new OAuthAuthorizationServerOptions()
{
//For Dev enviroment only (on production should be AllowInsecureHttp = false)
AllowInsecureHttp = true,
TokenEndpointPath = new PathString("/oauth/token"),
AccessTokenExpireTimeSpan = TimeSpan.FromDays(1),
Provider = new OAuthTokenProvider(),
RefreshTokenProvider = new RefreshTokenProvider(),
AccessTokenFormat = new Provider.JwtFormat("http://localhost:49860")
};
// OAuth 2.0 Bearer Access Token Generation
app.UseOAuthAuthorizationServer(OAuthServerOptions);
}
private void ConfigureOAuthTokenConsumption(IAppBuilder app)
{
var issuer = "http://localhost:49860";
string audienceId = Config.AudienceId;
byte[] audienceSecret = TextEncodings.Base64Url.Decode(Config.AudienceSecret);
// Api controllers with an [Authorize] attribute will be validated with JWT
app.UseJwtBearerAuthentication(
new JwtBearerAuthenticationOptions
{
AuthenticationMode = AuthenticationMode.Active,
AllowedAudiences = new[] { audienceId },
IssuerSecurityTokenProviders = new IIssuerSecurityTokenProvider[]
{
new SymmetricKeyIssuerSecurityTokenProvider(issuer, audienceSecret)
}
});
}
}
OAuthTokenProvider.cs
public class OAuthTokenProvider : OAuthAuthorizationServerProvider
{
public override Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)
{
// validate client credentials (demo)
// should be stored securely (salted, hashed, iterated)
context.Validated();
return Task.FromResult<object>(null);
}
public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
{
var allowedOrigin = "*";
context.OwinContext.Response.Headers.Add("Access-Control-Allow-Origin", new[] { allowedOrigin });
/***Note: Add User validation business logic here**/
if (context.UserName != context.Password)
{
context.SetError("invalid_grant", "The user name or password is incorrect.");
return;
}
var props = new AuthenticationProperties(new Dictionary<string, string>
{
{ "as:client_id", "Kaushik Thanki" }
});
ClaimsIdentity oAuthIdentity = new ClaimsIdentity("JWT");
var ticket = new AuthenticationTicket(oAuthIdentity, props);
context.Validated(ticket);
}
}
JwtFormat.cs
public class JwtFormat : ISecureDataFormat<AuthenticationTicket>
{
private readonly string _issuer = string.Empty;
public JwtFormat(string issuer)
{
_issuer = issuer;
}
public string Protect(AuthenticationTicket data)
{
if (data == null)
{
throw new ArgumentNullException("data");
}
string audienceId = Config.AudienceId;
string symmetricKeyAsBase64 = Config.AudienceSecret;
var keyByteArray = TextEncodings.Base64Url.Decode(symmetricKeyAsBase64);
var issued = data.Properties.IssuedUtc;
var expires = data.Properties.ExpiresUtc;
var token = new JwtSecurityToken(_issuer, audienceId, data.Identity.Claims, issued.Value.UtcDateTime, expires.Value.UtcDateTime);
var handler = new JwtSecurityTokenHandler();
var jwt = handler.WriteToken(token);
return jwt;
}
public AuthenticationTicket Unprotect(string protectedText)
{
throw new NotImplementedException();
}
}
RefreshTokenProvider.cs
public class RefreshTokenProvider : IAuthenticationTokenProvider
{
private static ConcurrentDictionary<string, AuthenticationTicket> _refreshTokens = new ConcurrentDictionary<string, AuthenticationTicket>();
public void Create(AuthenticationTokenCreateContext context)
{
throw new NotImplementedException();
}
public async Task CreateAsync(AuthenticationTokenCreateContext context)
{
var guid = Guid.NewGuid().ToString();
// maybe only create a handle the first time, then re-use for same client
// copy properties and set the desired lifetime of refresh token
var refreshTokenProperties = new AuthenticationProperties(context.Ticket.Properties.Dictionary)
{
IssuedUtc = context.Ticket.Properties.IssuedUtc,
ExpiresUtc = DateTime.UtcNow.AddYears(1)
};
var refreshTokenTicket = new AuthenticationTicket(context.Ticket.Identity, refreshTokenProperties);
//_refreshTokens.TryAdd(guid, context.Ticket);
_refreshTokens.TryAdd(guid, refreshTokenTicket);
// consider storing only the hash of the handle
context.SetToken(guid);
}
public void Receive(AuthenticationTokenReceiveContext context)
{
throw new NotImplementedException();
}
public async Task ReceiveAsync(AuthenticationTokenReceiveContext context)
{
AuthenticationTicket ticket;
if (_refreshTokens.TryRemove(context.Token, out ticket))
{
context.SetTicket(ticket);
}
}
}
Now Once I pass the authentication (Which I kept dummy for initial level matching same username & password) & got the token & refresh token.
When I request for method that is decorated with [Authorize] attribute, I always gets 401 status code.
I testing this method in postman following way
Any help or guidance will be really appreciated. I have invested my two days finding the solution for this but all in vain.

Is there any way to do token base auth in asp.net core application without redundant stuffs?

Anyone have a good example to do token based authorization in asp.net core without this crap like IdentityContext and other? I just want to set up settings for token generating in order to my system can generate and check token in right way and I want to manage authentication process by myself. Thanks
Having used a solution from this article go.microsoft.com/fwlink/?linkid=84547:
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(o =>
{
o.Authority = Configuration["AuthOptions:Authority"];
o.RequireHttpsMetadata = false;
o.TokenValidationParameters = new TokenValidationParameters()
{
ValidIssuer = Configuration["AuthOptions:Issuer"],
ValidateAudience = false,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["AuthOptions:Key"])),
ValidateLifetime = true,
};
});
services.AddMvc();
ConfigureDependincies(services);
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
var configuration = new SlackConfiguration
{
WebhookUrl = new Uri("https://hooks.slack.com/services/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"),
MinLevel = LogLevel.Warning
};
loggerFactory.AddSlack(configuration, env);
// loggerFactory.AddDebug();
app.UseDefaultFiles();
app.UseDeveloperExceptionPage();
app.UseAuthentication();
//app.UseJwtBearerAuthentication(new JwtBearerOptions()
//{
// AutomaticAuthenticate = true,
// AutomaticChallenge = true,
// RequireHttpsMetadata = false,
// TokenValidationParameters = new TokenValidationParameters()
// {
// ValidIssuer = Configuration["AuthOptions:Issuer"],
// ValidateAudience = false,
// ValidateIssuerSigningKey = true,
// IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["AuthOptions:Key"])),
// ValidateLifetime = true,
// }
//});
app.UseMvc();
}
var token = new JwtSecurityToken(
issuer: _root["AuthOptions:Issuer"],
notBefore: DateTime.UtcNow,
claims: identity.Claims,
expires: DateTime.UtcNow.Add(TimeSpan.FromMinutes(Convert.ToDouble(_root["AuthOptions:TokenLifeTime"]))),
signingCredentials: new SigningCredentials(new SymmetricSecurityKey(Encoding.ASCII.GetBytes(_root["AuthOptions:Key"])), SecurityAlgorithms.HmacSha512)
);
return token;
It's working for me.
It can be done in following way
Have a token generator API endpoint (controller) i.e. http://localhost/auth/token
In this, you generate token by verifying the authenticate user (check user against store)
The generated token can be validated by providing authentication schema in ASP.NET Core pipeline. app.AddAuthentication()
Any subsequent calls to API, should have Authorization header with generated token.
This approach can be refined accordingly.
I've done this very thing. I got sick of all the third party things so I wrote my own.
You want to create tokens and provide them/ validate them through an api.
Here is an example of the api controller that creates the token initially.
[Route("api/[controller]")]
public class TokenController : Controller
{
private readonly TokenCreatorOption _tco;
private readonly CryptoHash _ch;
public TokenController(IOptions<TokenCreatorOption> ioptTCO, IOptions<CryptoHash> ioptCH, IOptions<ConnectionStrings> ioptConn)
{
_tco = ioptTCO.Value;
_ch = ioptCH.Value;
}
[HttpPost("")]
public async Task<IActionResult> IssueToken([FromBody] CredentialUser model)
{
///if model is null, this is an incorrect format
if(model == null)
{
return BadRequest();
}
var user = GetUserFromDatabaseOrStore(model.userName, model.passWord);
if(user == null)
{
return NotFound();
}
TokenCreatorOption newTCO = _tco; ///get your initial instantiation of the TokenCreatorOption. This is set to default values based off appsettings or in configure services
newTCO.UserObject = user;
newTCO.Expiration = DateTime.UtcNow.AddMinutes(30).ToString("yyyy-MM-dd hh:mm:ss.ss tt");
///anything within the TokenCreatorOption will be hashed, anything in the token Provider is not going to be hashed (not secured), but acts as a good object to store just general things that are needed on client side.
TokenProvider _tpo = new TokenProvider();
_tpo.tco = TokenInteraction.CreateToken(newTCO, _ch.salt);
_tpo.listApp = xapp; ///put anything you wouldn't want to be hashed and claimed against outside of the object. so you always validate things inside the tco, but never exclude anything inside tco. This was a fatal flaw in tokens in the past.
///this is using messagepack to serialize, to make it smaller since this is going to be passed between every request/response. Consider zipping as well if large enough.
var serializer = MessagePackSerializer.Get<TokenProvider>();
byte[] obj = null;
using (var byteStream = new MemoryStream())
{
serializer.Pack(byteStream, _tpo);
obj = byteStream.ToArray();
}
return File(obj, "application/octet-stream");
}
TokenCreatorOption Class
public class TokenCreatorOption
{
public string Issuer { get; set; }
public UserFromThatDatabaseOrStore UserObject { get; set; }
public string Expiration { get; set; }
public string HashValue { get; set; }
}
Notice that all these objects in TokenCreatorOption are claims. Every single one is checked in the hash function.
Here is the Token Creator and the Token Validator, once a token is valid, you can reissue a new one.
TokenInteraction
public static class TokenInteraction
{
public static TokenCreatorOption CreateToken(TokenCreatorOption _tco, byte[] salt)
{
byte[] exp = Encoding.UTF8.GetBytes(_tco.Expiration);
byte[] issuer = Encoding.UTF8.GetBytes(_tco.Issuer);
byte[] user = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(_tco.UserObject));
byte[] salty = salt;
IEnumerable<byte> rv = exp.Concat(issuer).Concat(user).Concat(salty);
HashAlgorithm alg = SHA512.Create();
_tco.HashValue = Convert.ToBase64String(alg.ComputeHash(rv.ToArray()));
return _tco;
}
public static bool ValidateToken(TokenCreatorOption _tco, byte[] salt)
{
byte[] exp = Encoding.UTF8.GetBytes(_tco.Expiration);
byte[] issuer = Encoding.UTF8.GetBytes(_tco.Issuer);
byte[] user = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(_tco.UserObject));
byte[] salty = salt;
IEnumerable<byte> rv = exp.Concat(issuer).Concat(user).Concat(salty);
HashAlgorithm alg = SHA512.Create();
if (_tco.HashValue != Convert.ToBase64String(alg.ComputeHash(rv.ToArray())))
{
return false;
}
else
{
return true;
}
}
Notice in TokenInteraction The order of bytes added to rv needs to be in the same order when we validate the token.
Now we can have a validate controller.
[Route("api/[controller]")]
public class ValidateController : Controller
{
private readonly TokenCreatorOption _tco;
private readonly CryptoHash _ch;
public ValidateController(IOptions<TokenCreatorOption> ioptTCO, IOptions<CryptoHash> ioptCH)
{
_tco = ioptTCO.Value;
_ch = ioptCH.Value;
}
[HttpPost("")]
public async Task<IActionResult> ValidateToken([FromBody] TokenCreatorOption model)
{
if (model == null)
{
return BadRequest("Model Cannot be Null");
}
///Kick them right now if session is expired, so we don't have to do the full hashing.
if (DateTime.ParseExact(model.Expiration, "yyyy-MM-dd hh:mm:ss.ss tt", CultureInfo.InvariantCulture) < DateTime.UtcNow)
{
return BadRequest("Expired Datetime");
}
if(!TokenInteraction.ValidateToken(model, _ch.salt))
{
return Unauthorized();
}
model.Expiration = DateTime.UtcNow.AddMinutes(30).ToString("yyyy-MM-dd hh:mm:ss.ss tt");
TokenProvider _tpo = new TokenProvider();
_tpo.tco = TokenInteraction.CreateToken(model, _ch.salt);
var serializer = MessagePackSerializer.Get<TokenProvider>();
byte[] obj = null;
using (var byteStream = new MemoryStream())
{
serializer.Pack(byteStream, _tpo);
obj = byteStream.ToArray();
}
return File(obj, "application/octet-stream");
}
And of course when you are initially registering the services. Create your salt value either through a cert, or through a random crypto number generator.
public void ConfigureServices(IServiceCollection services)
{
// Add framework services.
services.AddCors();
services.AddOptions();
services.AddSwaggerGen();
services.ConfigureSwaggerGen(options =>
{
options.SingleApiVersion(new Swashbuckle.Swagger.Model.Info
{
Version = "v1"
});
services.Configure<TokenCreatorOption>(myopt =>
{
myopt.Issuer = "Issuer"; //either from appsettings or manually
myopt.Expiration = null;
myopt.UserObject = null;
myopt.HashValue = "";
});
byte[] salty;
new RNGCryptoServiceProvider().GetBytes(salty = new byte[64]);
services.Configure<CryptoHash>(copt =>
{
copt.salt = (new Rfc2898DeriveBytes("Super!SecretKey!123456789!##$", salty, 1000)).GetBytes(64);
});
services.AddSingleton<IConfiguration>(Configuration);

Specify login_hint using .NET Google.Apis.Oauth2.v2

Since the Google.Apis.Oauth2.v2 in GoogleWebAuthorizationBroker.AuthorizeAsync() requests a uri to a static json file that contains the links and parameters to Google Oauth2 services, how can I specify the login_hint parameter if I happen to know that information ahead of time?
var credential = await GoogleWebAuthorizationBroker.AuthorizeAsync(
new Uri("ms-appx:///Assets/client_secret.json"),
myScopes,
"user",
CancellationToken.None);
Extract from client_secret.json:
"auth_uri":"https://accounts.google.com/o/oauth2/auth?login_hint=user#domain.com"
How to specify the login_hint parameteron a per user basis?
I ended up subclassing the Google web authorization broker like this:
public class MyOAuth2WebAuthorizationBroker : GoogleWebAuthorizationBroker
{
public static async Task<UserCredential> AuthorizeAsync(ClientSecrets clientSecrets,
IEnumerable<string> scopes, string user, CancellationToken taskCancellationToken)
{
var initializer = new MyOAuth2AuthorizationCodeFlow.Initializer
{
ClientSecrets = clientSecrets,
Scopes = scopes,
DataStore = new StorageDataStore(),
};
var installedApp = new AuthorizationCodeWindowsInstalledApp(new MyOAuth2AuthorizationCodeFlow(initializer, user));
return await installedApp.AuthorizeAsync(user, taskCancellationToken).ConfigureAwait(false);
}
public class MyOAuth2AuthorizationCodeFlow : GoogleAuthorizationCodeFlow
{
private string loginHint { get; set; }
public MyOAuth2AuthorizationCodeFlow(Initializer initializer, string loginHint) : base(initializer)
{
this.loginHint = loginHint;
}
public override AuthorizationCodeRequestUrl CreateAuthorizationCodeRequest(string redirectUri)
{
return new GoogleAuthorizationCodeRequestUrl(new Uri(AuthorizationServerUrl))
{
ClientId = ClientSecrets.ClientId,
Scope = string.Join(" ", Scopes),
RedirectUri = redirectUri,
LoginHint = this.loginHint
};
}
}
}