I'm trying to learn SignalR and IdentitySever 4. I've created an application which consists of three services:
A - Asp .net core MVC application;
B - Identity server based on Identity Server 4;
C - SignalR service with ChatHub.
Application A and C used B as OpenIdConnect identity service. Authorization is required in Home Controller of Mvc application(A) and on ClientHub of Application C.
It should work in a next way:
User open Mvc application(A).
Then he is redirected to identity server(B) in order to log in.
After login user is redirected to the home page of the MVC application and should be made a connection to the SignalR hub and there is a problem.
Connection can't be made and it fails on Negotiate request, in the browser console I see only those errors that are not were informational:
enter image description here
During the investigation, I've added also controller Home with Index action with authorization to SignalR service, which returns view where also a connection to hub should be made, and in this case, it works.
Added handlers for OpenIdConnectEvents and I see only OnRedirectToIdentityProvider event is raised. I read from https://learn.microsoft.com/en-us/aspnet/core/signalr/authn-and-authz?view=aspnetcore-3.1 that if the user is logged in, the SignalR connection automatically inherits this authentication, maybe it is the problem that authentication used in MVc app is not valid for Hub.
Can anyone help with advice, please?
Update
As I can see an error from the console occurs because in case negotiate request is made server returns the login page (https://localhost:5001/connect/authorize?) which means cookies from MVC client are not valid for Hub:
authorize request cancelled
Looks like OpenIdConnect here. Is there any way to authenticate users?
Found the reason why it didn't work, it was because I used Cookie authentication from a different origin, so I used cookies from MVC-A, it is only allowed to use cookies from the same origin.
So I've changed it to JWT token like this:
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer("Bearer", options =>
{
options.Authority = "https://localhost:5001";
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateAudience = false
};
options.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
var accessToken = context.Request.Query["access_token"];
// If the request is for our hub...
var path = context.HttpContext.Request.Path;
if (!string.IsNullOrEmpty(accessToken) &&
(path.StartsWithSegments("/chat")))
{
// Read the token out of the query string
context.Token = accessToken;
}
return Task.CompletedTask;
}
};
});
Hope it will be helpful if someone has a similar issue.
Related
I have two webapis A and B. From A i make a request to B. A contains user info from identityserver4 where i just need to pass the token to the request header. Beside identityserver token, A also uses AAD(Azure Active Directory) where i have registred B. So from A, i also check my AAD so that i can retrieve The token from Azure to send to B, This is just so B can trust that a request is coming from a trusted registred source. As you can understand From A i have two tokens, one to check the loged in user and the other to check registred application. My A startup class look like this:
public void ConfigureServices(IServiceCollection services)
{
Config = services.ConfigureAuthConfig<AuthConfig>(Configuration.GetSection("AuthConfig"));
services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddScoped<ICurrentUser, CurrentUser>();
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddIdentityServerAuthentication(//this coming with identityserver token and user info
idt =>
{
idt.Authority = "https://gnoauth.se:5005";
idt.ApiName = "globalnetworkApi";
idt.SaveToken = true;
idt.RequireHttpsMetadata = true;
}
);
So here is my A httpclienthelper, where i setup my client headers to send to B, As i already have the token and user from identity server so the other thing i do here is to send B authority, client id and secret to AAD for retrieving the second token:
var context = new HttpContextAccessor().HttpContext;
var accessTokenFromIdentityserver = await
context.GetTokenAsync(OpenIdConnectParameterNames.AccessToken);
var tokenfromAAD = result.AccessToken;
defaultRequestHeaders.Authorization = new
AuthenticationHeaderValue("AAD_Authentication", tokenfromAAD);
from here i actualy have all i need, both the tokens and all claims. As you can see to the defaultrequestheaders i only cansend one token but i would like to send both tokens to B, how can i configure the request headers to be able to do that?
So here is the B startup
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer("AAD_Authentication", opt =>
{
opt.Audience = Configuration["AAD:ResourceId"];
opt.Authority = $"{Configuration["AAD:InstanceId"]}{Configuration["AAD:TenantId"]}";
opt.RequireHttpsMetadata = true;
})
.AddJwtBearer("IDS4_Authentication",
idt =>
{
idt.Authority = "https://gnoauth.se:5005";
idt.Audience = "globalnetworkApi";
idt.SaveToken = true;
idt.RequireHttpsMetadata = true;
}
);
services.AddAuthorization(options =>
{
options.DefaultPolicy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.AddAuthenticationSchemes("AAD_Authentication", "IDS4_Authentication")
.Build();
But i also setup a policy so that from some of my controllers i need to authorize both logedin user and application registration like this
[Authorize(AuthenticationSchemes = "AAD_Authentication,IDS4_Authentication", Policy = "MyPolicy")]
the big problem i have been facing is how from A to send both tokens and how to setup authenticationschemes so that B actually get two bearer authenticationschemes
ANY HELP PLEASE
It's the first time I see this thing of two authentication schemes at same time. With two JWT Bearer configurations. Usually I saw it with JWT and Cookie, but not with two JWT.
One thing you may notice, is that it's not the same authentication scheme for A than A calling B. In your case, despite A as an API, is really a client asking for authorization on B. With this point of view, you have nothing to do on A authentication to get on B.
You should use an HttpClient or TokenClient with the specific flow each time you want A call an endpoint on B.
The AddAuthentication configuration is to protect A, not for get authorization on B.
Other different matter is that you would like two Identity Providers AAD and IDS. This should use something similar to External Identity Providers.
Maybe I'm wrong or I didn't understand you. But I hope I clarify something about this question.
P.D. Azure Functions Easy Auth has something like "proxy" Identity Provider. I think it's the same idea as External Identity Provider.
I am attempting to set up SSO from our ASP.NET Core Identity Server app to an external identity provider. Basically a user that is authenticated on our external partner's website but isn't authenticated on our website clicks a link to go to a certain page on our website. Since they aren't authenticated on our website then it redirects them to the external identity provider's authentication request url, then after verifying they are authenticated it redirects them back to our identity server to sign them in then redirects them to the original page that they requested.
This works great with the authorization code flow (response_type=code), it seems that this is the default in Identity Server. However, one of the external identity providers that we're trying to integrate with does not support the authorization code flow, when I try it they return an error: "The+authorization+server+does+not+support+obtaining+an+authorization+code+using+this+method". They only support response_type=token. It's not clear to me how you can specify this in Identity Server, it appends response_type=code into the authentication request url no matter what. Even if I specify ?response_type=token in my authentication request url it STILL appends response_type=code after that. And I don't see any property on the Microsoft.AspNetCore.Authentication.OAuth.OAuthOptions class that can be used to configure it. What am I missing?
authenticationBuilder.AddOAuth(ssoIdentityProvider.SsoCode, options => SetOAuth2Options(ssoIdentityProvider, options));
private static void SetOAuth2Options(IdentityProvider ssoIdentityProvider, OAuthOptions options)
{
// Set standard options.
options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;
options.AuthorizationEndpoint = ssoIdentityProvider.AuthenticationRequestUrl;
options.CallbackPath = new PathString("/signin-oauth2");
options.ClientId = ssoIdentityProvider.OAuth2ClientId;
options.ClientSecret = ssoIdentityProvider.OAuth2ClientSecret;
options.UsePkce = ssoIdentityProvider.OAuth2UsePkce;
options.TokenEndpoint = ssoIdentityProvider.OAuth2TokenRequestUrl;
options.UserInformationEndpoint = ssoIdentityProvider.OAuth2UserInformationRequestUrl;
// Set requested scopes.
ssoIdentityProvider.OAuth2RequestedScopesList?.ForEach(s => options.Scope.Add(s));
// Set expected claims.
ssoIdentityProvider.OAuth2ExpectedClaimsList.ForEach(c => options.ClaimActions.MapJsonKey(c.Key, c.Value));
options.Validate();
}
I'm struggling to implement a custom auth flow with OAuth and JWT.
Basically it should go as follows:
User clicks in login
User is redirect to 3rd party OAuth login page
User logins into the page
I get the access_token and request for User Info
I get the user info and create my own JWT Token to be sent back and forth
I have been following this great tutorial on how to build an OAuth authentication, the only part that differs is that Jerrie is using Cookies.
What I Have done so far:
Configured the AuthenticationService
services.AddAuthentication(options =>
{
options.DefaultChallengeScheme = "3rdPartyOAuth";
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie() // Added only because of the DefaultSignInScheme
.AddJwtBearer(options =>
{
options.TokenValidationParameters = // Ommited for brevity
})
.AddOAuth("3rdPartyOAuth", options =>
{
options.ClientId = securityConfig.ClientId;
options.ClientSecret = securityConfig.ClientSecret;
options.CallbackPath = new PathString("/auth/oauthCallback");
options.AuthorizationEndpoint = securityConfig.AuthorizationEndpoint;
options.TokenEndpoint = securityConfig.TokenEndpoint;
options.UserInformationEndpoint = securityConfig.UserInfoEndpoint;
// Only this for testing for now
options.ClaimActions.MapJsonKey("sub", "sub");
options.Events = new OAuthEvents
{
OnCreatingTicket = async context =>
{
// Request for user information
var request = new HttpRequestMessage(HttpMethod.Get, context.Options.UserInformationEndpoint);
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", context.AccessToken);
var response = await context.Backchannel.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, context.HttpContext.RequestAborted);
response.EnsureSuccessStatusCode();
var user = JObject.Parse(await response.Content.ReadAsStringAsync());
context.RunClaimActions(user);
}
};
});
Auth Controller
[AllowAnonymous]
[HttpGet("login")]
public IActionResult LoginIam(string returnUrl = "/auth/loginCallback")
{
return Challenge(new AuthenticationProperties() {RedirectUri = returnUrl});
}
[AllowAnonymous]
[DisableRequestSizeLimit]
[HttpGet("loginCallback")]
public IActionResult IamCallback()
{
// Here is where I expect to get the user info, create my JWT and send it back to the client
return Ok();
}
Disclaimer: This OAuth flow is being incorporated now. I have a flow for creating and using my own JWT working and everything. I will not post here because my problem is before that.
What I want
In Jerrie's post you can see that he sets DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;. With that, when the /auth/loginCallback is reached I have the user claims in the HttpContext.
The problem is my DefaultAuthenticateScheme is set to JwtBearersDefault and when the loginCallback is called I can't see the user claims nowhere in the Request.
How can I have access to the information gained on the OnCreatingTicketEvent in my callback in this scenario?
Bonus question: I don't know much about OAuth (sure that is clear now). You may have noted that my options.CallbackPath differs from the RedirectUri passed in the Challenge at the login endpoint. I expected the option.CallbackPath to be called by the 3rd Part OAuth provider but this is not what happens (apparently). I did have to set the CallbackPath to the same value I have set in the OAuth provider configuration (like Jerries tutorial with GitHub) for it to work. Is that right? The Callback is used for nothing but a match configuration? I can even comment the endpoint CallbackPath points to and it keep working the same way...
Thanks!
Auth
As Jerrie linked in his post, there is a great explanation about auth middlewares:
https://digitalmccullough.com/posts/aspnetcore-auth-system-demystified.html
You can see a flowchart in the section Authentication and Authorization Flow
The second step is Authentication middleware calls Default Handler's Authenticate.
As your default auth handler is Jwt, the context is not pupulated with the user data after the oauth flow,
since it uses the CookieAuthenticationDefaults.AuthenticationScheme
Try:
[AllowAnonymous]
[DisableRequestSizeLimit]
[HttpGet("loginCallback")]
public IActionResult IamCallback()
{
//
// Read external identity from the temporary cookie
//
var result = await HttpContext.AuthenticateAsync(CookieAuthenticationDefaults.AuthenticationScheme);
if (result?.Succeeded != true)
{
throw new Exception("Nein");
}
var oauthUser = result.Principal;
...
return Ok();
}
Great schemes summary: ASP.NET Core 2 AuthenticationSchemes
You can persist your user with
https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticationhttpcontextextensions.signinasync?view=aspnetcore-2.2
Bonus
I did have to set the CallbackPath to the same value I have set in the OAuth provider configuration (like Jerries tutorial with GitHub) for it to work. Is that right?"
Yes.
For security reasons, the registered callback uri (on authorization server) and the provided callback uri (sent by the client) MUST match.
So you cannot change it randomly, or if you change it, you have to change it on the auth server too.
If this restriction was not present, f.e. an email with a mailformed link (with modified callback url) could obtain grant.
This is called Open Redirect, the rfc refers to it too: https://www.rfc-editor.org/rfc/rfc6749#section-10.15
OWASP has a great description: https://github.com/OWASP/CheatSheetSeries/blob/master/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.md
I can even comment the endpoint CallbackPath points to and it keep working the same way..."
That is because your client is trusted (you provide your secret, and you are not a fully-frontend Single Page App). So it is optional for you to send the callback uri.
But IF you send it, it MUST match with the one registered on the server. If you don't send it, the auth server will redirect to the url, that is registered on its side.
https://www.rfc-editor.org/rfc/rfc6749#section-4.1.1
redirect_uri
OPTIONAL. As described in Section 3.1.2.
https://www.rfc-editor.org/rfc/rfc6749#section-3.1.2
The authorization server redirects the user-agent to the
client's redirection endpoint previously established with the
authorization server during the client registration process or when
making the authorization request.
https://www.rfc-editor.org/rfc/rfc6749#section-3.1.2.2
The authorization server MUST require the following clients to register their redirection endpoint:
Public clients.
Confidential clients utilizing the implicit grant type.
Your client is confidential and uses authorization code grant type (https://www.rfc-editor.org/rfc/rfc6749#section-1.3.1)
https://www.rfc-editor.org/rfc/rfc6749#section-3.1.2.3
If multiple redirection URIs have been registered, if only part of
the redirection URI has been registered, or if no redirection URI has
been registered, the client MUST include a redirection URI with the
authorization request using the "redirect_uri" request parameter.
You have registered your redirect uri, that's why the auth server does not raise an error.
change [AllowAnonymous]
to [Authorize]
on the 'loginCallback' endpoint (AuthController.IamCallback method)
I've API created in asp.net core 2.0 where I am using mixed mode authentication. For some controllers JWT and for some using windows authentication.
I've no problem with the controllers which authorize with JWT. But for the controllers where I want to use windows authentication I am indefinitely prompted with user name and password dialog of chrome.
Here my sample controller code where I want to use Windows Authentication instead of JWT.
[Route("api/[controller]")]
[Authorize(AuthenticationSchemes = "Windows")]
public class TestController : Controller
{
[HttpPost("processUpload")]
public async Task<IActionResult> ProcessUploadAsync(UploadFileModel uploadFileModel)
{
}
}
My configure services code
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = IISDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer("Bearer", options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateAudience = false,
ValidateIssuer = false,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("blahblahKey")),
ValidateLifetime = true, //validate the expiration and not before values in the token
ClockSkew = TimeSpan.FromMinutes(5) //5 minute tolerance for the expiration date
};
});
// let only my api users to be able to call
services.AddAuthorization(auth =>
{
auth.AddPolicy("Bearer", new AuthorizationPolicyBuilder()
.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme)
.RequireClaim(ClaimTypes.Name, "MyApiUser").Build());
});
services.AddMvc();
}
My configure method.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseCors("CorsPolicy");
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseAuthentication(); //needs to be up in the pipeline, before MVC
app.UseMvc();
}
Appreciate your suggestions and help on this.
Update: Till now I've been debugging my code on chrome. But when I have used IE 11, the above code is running without any issue.
Can this be CORS issue of chrome where preflight issue?
Thanks
You need to ensure, that you NOT setting Authorization: Bearer <JWT_token> HTTP header when you trying to use Windows Auth. The key point here is how "Windows Auth" actually works. Let's look how it works with browser for example.
Let's call this "a normal flow":
You navigate to http://example.com/api/resource in your browser;
Your browser send a HTTP GET request to http://example.com/api/resource without any Authorization HTTP Header for now (an anonymous request);
Web server (or WebAPI themself) recieve a request, find out, that there is no Authorization header and respond with 401 Not Authorized status code with WWW-Authenticate: NTLM,Negotiate HTTP Header setted up ("Go away, no anonymous access. Only 'NTLM' or 'Negotiate' guys are welcome!");
Browser receive a 401 response, find out that request was anonymous, looks to WWW-Authenticate header and instantly repeat request, now with Authorization: NTLM <NTLM_token> HTTP Header ("Ok, take it easy, mr. Web server! Here is my NTLM token.");
Server receive a second request, find NTLM token in Authorization header, verify it and execute request ("Ok, you may pass. Here is your resource.").
Things goes a little different, when you initialy set Authorization header to some value:
Your JS require http://example.com/api/resource with JWT authorization;
Your browser send a HTTP GET request to http://example.com/api/resource with Authorization: Bearer <JWT_token> HTTP Header now;
Web server (or WebAPI themself) recieve a request, find out, that there is Authorization header with "Bearer" authentication scheme and again respond with 401 Not Authorized status code with WWW-Authenticate: NTLM,Negotiate HTTP Header setted up ("Go away, we don't know who are this 'Bearer' guys, but we don't like them. Only 'NTLM' or 'Negotiate' guys are welcome!");
Browser receive a 401 response, find out that request was authorized and decide that this token is bad. But, as you actually set Authorization header, this means that you actually have some credentials. And so it ask you for this credentials with this dialog.
I just had the same need. I'm not yet running things on IIS, only Kestrel, but I managed to adapt Microsoft's own instructions to get per controller/controller method authentication using JWT and Windows auth.
All I did was modify Startup.cs/ConfigureServices from
services.AddAuthentication(x =>
{
x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(x =>
{
x.RequireHttpsMetadata = false; // should be set to true in production
x.SaveToken = true;
x.TokenValidationParameters = generateTokenValidationParameters();
});
to this
services.AddAuthentication()
.AddNegotiate()
.AddJwtBearer(x =>
{
x.RequireHttpsMetadata = false; // should be set to true in production
x.SaveToken = true;
x.TokenValidationParameters = generateTokenValidationParameters();
});
So, basically, removed the Default Authentication and Challenge scheme, added Negotiate (Windows Auth) and JwtBearer using my pre-existing JWT configuration.
In the controllers, I enabled Windows Authentication by adding this authorization header
[Authorize(AuthenticationSchemes = NegotiateDefaults.AuthenticationScheme)]
And similarly, I replaced my existing Authorization headers that previously did JWD (given that it was the default auth/challenge scheme) with this
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
My app will be hosted by Kestrel, so IIS won't be an issue, but I'll try that next, anyway.
#edit: I've now mastered IIS Express, too. Enabled Windows Auth through VS's IIS Express settings (Project Properties, Debug tab), then ensure that IIS does not perform automatic authentication for in and out of processing hosting by adding this to Startup.ConfigureServices (right after AddAuthentication).
//disable automatic authentication for in-process hosting
services.Configure<IISServerOptions>(options =>
{
options.AutomaticAuthentication = false;
});
//disable automatic authentication for out-of-process hosting
services.Configure<IISOptions>(options =>
{
options.AutomaticAuthentication = false;
});
I then changed my test controller to have a method with the following authorize header
[Authorize(AuthenticationSchemes = IISDefaults.AuthenticationScheme)]
And when I access that method with a browser that trusts the URL, I'm being let in and User.Identity is my windows identity.
Now off to see if that also works on an actual IIS.
I am using ASP.Net 5 MVC 6 with JWT tokens that are created while the user is on another site which this site is a subdomain of. My goal is to pass the token along with the request to this subdomain. If a user happens to try to come to this subdomain url without the proper token in the header then I want to redirect them to the main site login page.
After much frustration with the newest RC-1 release and using JWT tokens with a SecureKey instead of certificates. I finally got my code working by using the RC-2 nightly build version. Now my problem is that I want to be able to redirect to an outside url in the case of unauthenticated users. Here is an example of my authentication code:
var key = "mysupersecretkey=";
var encodedkey2 = Convert.FromBase64String(key);
app.UseJwtBearerAuthentication(options =>
{
options.AutomaticAuthenticate = true;
options.AutomaticChallenge = true;
options.TokenValidationParameters.IssuerSigningKey = new SymmetricSecurityKey(encodedkey2);
options.TokenValidationParameters.ValidIssuer = "https://tv.alsdkfalsdkf.com/xxx/yyy";
options.TokenValidationParameters.ValidateIssuer = true;
options.TokenValidationParameters.ValidAudience = "https://www.sdgfllfsdkgh.com/";
options.TokenValidationParameters.ValidateAudience = true;
options.Configuration = new Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectConfiguration()
{
Issuer = "https://tv.sdfkaysdf.com/xxx/yyy"
};
});
now I see other examples which are using OpedId and they have it pretty easy , there is a parameter called RedirectUrl
app.UseOpenIdConnectAuthentication(options => {
...
options.RedirectUri = "https://localhost:44300";
...
});
any idea how to set RedirectUrl when using JwtBearerAuthentication ???
There's no such property for a simple reason: the JWT bearer middleware (like the more generic OAuth2 middleware in Katana) has been designed for API authentication, not for interactive authentication. Trying to trigger a redirection in this case wouldn't make much sense for headless HTTP clients.
That said, it doesn't mean that you can't redirect your unauthenticated users at all, at some point. The best way to handle that is to catch the 401 response returned by the JWT middleware at the client level and redirect the user to the appropriate login page. In JS applications for instance, this is usually done using an HTTP interceptor.
If you're really convinced breaking the OAuth2 bearer specification is the right thing to do, you can do that using the OnChallenge notification:
app.UseJwtBearerAuthentication(options => {
options.Events = new JwtBearerEvents {
OnChallenge = context => {
context.Response.Redirect("http://localhost:54540/login");
context.HandleResponse();
return Task.FromResult(0);
}
};
});