IdentityServer3: Principals always null - asp.net

I tried to enhance my existing WebApi with IdentityServer3. So I installed the IdentityServer3.AccessTokenValidation package and added this piece of code to my Startup Configuration
app.UseIdentityServerBearerTokenAuthentication(new IdentityServerBearerTokenAuthenticationOptions
{
Authority = "<myIdentityServerUrl>",
ValidationMode = ValidationMode.ValidationEndpoint,
RequiredScopes = new[] { "api1" }
});
(I did not apply the AuthorizeAttribute filter, so I can see what's going on).
The identity server so far is the exact same as in the docs (code here). I tried to debug-call the test service and I saw that this.User (in the controllers method) was null. So I looked into the RequestContext. Now that was weird:
RequestContext.Principals is null
RequestContext.Request.Headers.Authorization has the correct access_token
As far as I know even if I made a mistake with the scopes or Authority -what I highly doubt- I should still get the claims. The AuthorizeAttribute would probably return an Unauthorized http message but that doesn't matter because I did not add that filter yet.

Related

Why do I get null `Authentication` as #Controller method parameter in `#WebMvcTest`?

The context: I created a test annotation #WithMockAuthentication to populate test security context with an Authentication instance, much like #WithMockUser does.
The main difference being, in my case, the instance is a Mockito mock.
What I experience: As soon as I replace an actual instance with a mock, the Authentication instance provided as controller method parameter is null in annotated tests: in the WithSecurityContextFactory, if I replace:
public Authentication workingAuthentication(WithMockAuthentication annotation) {
return new TestAuthentication(annotation.name(), Stream.of(annotation.authorities()).map(SimpleGrantedAuthority::new).collect(Collectors.toSet()));
}
with
public Authentication bogousAuthentication(WithMockAuthentication annotation) {
var auth = mock(Authentication.class);
when(auth.getName()).thenReturn(annotation.name());
when(auth.getAuthorities()).thenReturn((Collection) Stream.of(annotation.authorities()).map(SimpleGrantedAuthority::new).collect(Collectors.toSet()));
when(auth.isAuthenticated()).thenReturn(true);
return auth;
}
Then I get NPE in the controller tests at
#RequestMapping("/method")
#PreAuthorize("hasRole('ROLE_AUTHORIZED')")
public ResponseEntity<String> securedMethod(Authentication auth) {
// Here, auth is null if Authentication is a mock
return ResponseEntity.ok(String.format("Hey %s, how are you?", auth.getName()));
}
I've created a minimal sample to reproduce. Run the test to see the failure.
I'm pretty sure I face a bug. Enough to create an issue in spring-security project, but it seems that Spring team team has no time to investigate...
[EDIT]
This last statement is uselessly offensive and completely wrong as the answer is provided by Rob Winch, who is a major member of spring-security :/ My Bad
The argument for your SampleController is of type Authentication which is an instance of Principal and thus the ServletRequestMethodArgumentResolver will attempt to resolve the argument from HttpServletRequest.getUserPrincipal().
The mock that you are creating did not stub the Authentication.getPrincipal() method.
public Authentication bogousAuthentication(WithMockAuthentication annotation) {
var auth = mock(Authentication.class);
when(auth.getName()).thenReturn(annotation.name());
when(auth.getAuthorities()).thenReturn((Collection) Stream.of(annotation.authorities()).map(SimpleGrantedAuthority::new).collect(Collectors.toSet()));
when(auth.isAuthenticated()).thenReturn(true);
return auth;
}
For that reason, Authentication.getPrincipal() is null and thus SecurityContextHolderAwareRequestWrapper.getUserPrincipal() returns null. Why does it return null when the principal is null? I cannot be certain the original intention as the code was added before I was a team member. However, it makes sense in Spring Security's model. Authentication can represent both an authenticated user and credentials used for authenticating. The Javadoc of Authentication.getPrincipal() states (emphasis mine):
The identity of the principal being authenticated. In the case of an
authentication request with username and password, this would be the
username. Callers are expected to populate the principal for an
authentication request.
The AuthenticationManager implementation will often return an
Authentication containing richer information as the principal for use
by the application. Many of the authentication providers will create a
UserDetails object as the principal.
The null check is to ensure that the Authentication is indeed representing an authenticated user.
To fix it you must stub out the getPrincipal() method with something like when(auth.getPrincipal()).thenReturn("bogus");. The change can be seen below and in my pull request.
public Authentication bogousAuthentication(WithMockAuthentication annotation) {
var auth = mock(Authentication.class);
when(auth.getPrincipal()).thenReturn("bogus");
when(auth.getName()).thenReturn(annotation.name());
when(auth.getAuthorities()).thenReturn((Collection) Stream.of(annotation.authorities()).map(SimpleGrantedAuthority::new).collect(Collectors.toSet()));
when(auth.isAuthenticated()).thenReturn(true);
return auth;
}

Endpoint belongs to different authority

trying to use Azure AD as OpenID provider with IdentityModel package
However the problem is that it produces wrong endpoint configuration
var client = new HttpClient();
const string identityUrl = "https://login.microsoftonline.com/00edae13-e792-4bc1-92ef-92a02ec1d939/v2.0";
const string restUrl = "https://localhost:44321";
var disco = await client.GetDiscoveryDocumentAsync(identityUrl);
if (disco.IsError)
{
Console.WriteLine(disco.Error);
return;
}
returns error
Endpoint belongs to different authority:
https://login.microsoftonline.com/00edae13-e792-4bc1-92ef-92a02ec1d939/oauth2/v2.0/authorize
openid-configuration output is
{"authorization_endpoint":"https://login.microsoftonline.com/00edae13-e792-4bc1-92ef-92a02ec1d939/oauth2/v2.0/authorize",
"token_endpoint":"https://login.microsoftonline.com/00edae13-e792-4bc1-92ef-92a02ec1d939/oauth2/v2.0/token" ... }
oauth2 is added between the tenatID and version. I suppose this is why openid metadata validation fails.
Is it possible to configure AzureAD to return correct metadata for the openid-configuration ?
Regards
could you find a solution for this? The only way I could figure out (far to be the optimal solution) is to add the endpoints to a list of additional endpoint base addresses. Otherwise you have to set the validations to false as stated in the comments above.
var client = httpClientFactory.CreateClient();
var disco = await client.GetDiscoveryDocumentAsync(
new DiscoveryDocumentRequest
{
Address = "https://login.microsoftonline.com/00edae13-e792-4bc1-92ef-92a02ec1d939/v2.0",
Policy =
{
ValidateIssuerName = true,
ValidateEndpoints = true,
AdditionalEndpointBaseAddresses = { "https://login.microsoftonline.com/00edae13-e792-4bc1-92ef-92a02ec1d939/oauth2/v2.0/token",
"https://login.microsoftonline.com/00edae13-e792-4bc1-92ef-92a02ec1d939/oauth2/v2.0/authorize",
"https://login.microsoftonline.com/00edae13-e792-4bc1-92ef-92a02ec1d939/discovery/v2.0/keys",
"https://login.microsoftonline.com/00edae13-e792-4bc1-92ef-92a02ec1d939/oauth2/v2.0/devicecode",
"https://graph.microsoft.com/oidc/userinfo",
"https://login.microsoftonline.com/00edae13-e792-4bc1-92ef-92a02ec1d939/oauth2/v2.0/logout"
}
},
}
);
If you take a look at the code inside IdentityModel repository, you can see that the default validation of the endpoints validates them by doing a "starts with" method. https://github.com/IdentityModel/IdentityModel/blob/1db21e2677de6896bc11227c70b927c502e20898/src/Client/StringComparisonAuthorityValidationStrategy.cs#L46
Then the only two required AdditionalEndpointBaseAddresses inside the DiscoveryDocumentRequest Policy field you need to add are "https://login.microsoftonline.com/<guid>" and "https://graph.microsoft.com/oidc/userinfo".
I had the same problem as well and when i upgraded IdentityModel to version 2.16.1 the problem was solved
Azure AD seems to need Additional Endpoints configuration as #flacid-snake suggested. Setting validate endpoints to False is a security threat and should be avoided.
The best way is to make it configurable, preferable in the UI when you configure the SSO server. Endpoints can change and they should be easy to change. It will also make it easier if you later decide to support Okta or other providers and they require additional endpoints.
As of June 2021 you also need to include Kerberos endpoint like:
https://login.microsoftonline.com/888861fc-dd99-4521-a00f-ad8888e9ecc8bfgh/kerberos (replace with your directory tenant id).

Handle Unauthorized Request and Return Status Code 404

I am developing a standalone .Net Core API targeting framework .Net Core 2.2.The authentication scheme is JWTBearerTokens connecting to our ADFS Identify server.
When I call an API endpoing decorated with the [Authorize] attribute I am getting a 401 Unauthorized response, which is expected and default behaviour.
What I want to do next is instead of having that same call return a 401, I would like to return the status code to be 404. (I don't want to get into great details of why 404. Simply, I do not want to expose that the endpoint exists if a valid token is not included in request)
In previous .Net Framework WebAPI you could create your own attribute and override the HandleUnauthorizedRequest method and return the status code you want.
I have reviewed the documentation on policy-based authorization, but have not tried the sample or tried implementing it. The policy handler looks more to do with handling (return success or fail) if a policy is not fulfilled. I do not see anywhere where you can return a different status code on failure. So that only would make sense if I start checking against actual Policies.
Any insights?
Returning 404 instead of 401 is bad practice(as mentioned in the comments by #Chris Pratt) and must be avoided. Consider these cases,
You're leaving the project to someone else and they can't figure why 404 is returned
A 404 is returned when you call the homepage/Index page. Poor ideology.
Later on in the project, you decide to allow post requests without authentication. So on and so forth.
Anyways, as part of the community, I'll give you the answer...
Add this to your global.asax
void Application_EndRequest(object source, System.EventArgs args)
{
if (Response.StatusCode == 401)
{
Response.ClearContent();
Response.RedirectToRoute("ErrorH", (RouteTable.Routes["ErrorH"] as Route).Defaults);
}
}
And in routeConfig, create a route for your errorHandler :
routes.MapRoute(
"ErrorH",
"Error/{action}/{errMsg}",
new { controller = "CustomController", action = "Change401To404", errMsg = UrlParameter.Optional }
);
And in your custom controller :
public class CustomController : Controller //or Base
{
public ActionResult Change401To404(){
//Do whatever you want
}
}
PS: This is not the only way, there are many other ways to do it. But at least in this method, you can differentiate real 404 responses from 401 responses.

Issue with jwt-bearer on-behalf-of grant in Azure AD

So I have an Angular app that uses the adal-angular library to authenticate with an ASP.NET Core 2.0 Web API. The API then uses on-behalf-of flow to authenticate with another API using the users token like this MS article https://learn.microsoft.com/en-us/azure/active-directory/develop/active-directory-protocols-oauth-on-behalf-of.
The issue I have is this is working fine in the DEV environment but I have now deployed a TST environment with separate App Registrations and I am receiving the following exception when I try and request the token using on-behalf-of
AADSTS240002: Input id_token cannot be used as 'urn:ietf:params:oauth:grant-type:jwt-bearer' grant.
The code I am using to request the token
public async Task<string> AcquireTokenAsync(string resource)
{
try
{
string accessToken = await _httpContextAccessor.HttpContext.GetTokenAsync(AuthenticationConstants.AccessToken);
var credentials = new ClientCredential(_azureOptions.ClientId, _azureOptions.ClientSecret);
var authContext = new AuthenticationContext($"{_azureOptions.Instance}{_azureOptions.TenantId}")
{
ExtendedLifeTimeEnabled = true
};
// On-behalf-of auth token request call
var authResult = await authContext.AcquireTokenAsync(
resource,
credentials,
new UserAssertion(accessToken));
return authResult.AccessToken;
}
catch (AdalServiceException asex)
{
_logger.LogError(asex, $"Instance: {_azureOptions.Instance} Tenant: {_azureOptions.TenantId} ClientId: {_azureOptions.ClientId}");
throw;
}
catch (System.Exception ex)
{
_logger.LogError(ex, ex.Message);
throw;
}
}
And I have used Fiddler and it looks like all the correct parameters are being passed.
Any help would be very much appreciated. I have set knownClientApplications on the second API and I have granted permissions on the Angular backend API to the second API.
For me, I got it to work by changing BOTH of the following to true:
oauth2AllowImplicitFlow
oauth2AllowIdTokenImplicitFlow
See here for more information.
According to your question and the error, it should be caused by that you angular app is not a Native(public) app.
For using this OBO flow with this Grant type, your client must be a public client not credential client.
If you want to register your client as a WebApp/API, you can refer to this Implementation:
Hope this helps!
Update
According to OP's comment, he/she got it working by changing oauth2AllowImplicitFlow from false to true.
We had this problem last week with one Azure Service Registration and not another. A review found that the token didn't return an AIO being returned. It turns out that the registration had redirects with wildcards (e.g., https://*.ngrok.io) and this is incompatible with the AcquireTokenOnBehalfOf function. I'm posting this here so a future person, probably me, will find it.
I was having problems even when oauth2AllowImplicitFlow and oauth2AllowIdTokenImplicitFlow were set to true. One of my Reply URLs had a wildcard in it. When the wildcard was removed, the issue was resolved.

Google OpenId Connect migration: getting the openid_id in ASP.NET app

I've gone through plenty of Google documentation and SO Q/A's but with no luck. I wonder if anyone has yet succesfully used the OpenId to OpenId Connect migration as advised by Google.
This is what we used to do:
IAuthenticationResponse response = _openid.GetResponse();
if (response != null) {
//omitted for brevity
} else {
IAuthenticationRequest req = _openid.CreateRequest("https://www.google.com/accounts/o8/id");
req.AddExtension(new ClaimsRequest
{
Country = DemandLevel.Request,
Email = DemandLevel.Request,
Gender = DemandLevel.Require,
PostalCode = DemandLevel.Require,
TimeZone = DemandLevel.Require
});
req.RedirectToProvider();
}
That was done using a version of DotNetOpenAuth that dates back a few years. Because Google has deprecated OpenId authentication we are trying to move over to OpenID Connect. The key question here is: can I somehow get my hands on the OpenId identifier (in the form of https://www.google.com/accounts/o8/id?id=xyz) using the latest version of DotNetOpenAuth library or by any other means?
I have tried the latest DotNetOpenAuth and I can get it to work but it gives me a new Id (this was expected). I have also tried the Javascript way by using this URL (line breaks for readibility):
https://accounts.google.com/o/oauth2/auth?
scope=openid%20profile%20email
&openid.realm=http://localhost/palkkac/
&client_id=//here is the client id I created in google developer console
&redirect_uri=http://localhost/palkkac/someaspxpagehere
&response_type=id_token%20token
I checked (using Fiddler) the realm value that we currently send using the old DotNetOpenAuth code and it is http://localhost/palkkac/. I've put the same realm in the url above. The redirect url starts with the realm value but it is not entirely the same.
When I redirect to a simple page that parses the id_token and decrypts it (using the https://www.googleapis.com/oauth2/v1/tokeninfo?id_token=zyx endpoint) I get this:
audience "client id is here"
email "mikkark#gmail.com"
expires_in 3597
issued_at //some numbers here
issued_to "client id is here"
issuer "accounts.google.com"
user_id "here is a sequence of numbers, my id in the OpenID Connect format that is"
verified_email true
So there is no sign of the openid_id field that you would expect to find here, though the whole structure of the message seems different from the Google docs, there is no field titled sub, for example. I wonder if I'm actually using the wrong endpoint, parameters or something?
What I have been reading is the migration guide: https://developers.google.com/accounts/docs/OpenID. I skipped step 2 because it seemed like an optional step. In step 3 the field openid_id is discussed and I would like to get that to work as a proof-of-concept first.
We registered the app on Google in order to create the client id etc. There are now also numerous allowed redirect url's as well as javascript origins listed in the Google dev console. Let me know if those might mess up the system and I'll post them here for review.
Side note: we are supposed to be moving our app behind a strictly firewalled environment where we would need to open ports in order to do this on the server side. Therefore, a client-side Javascript solution to access Google combined with HTTPS and redirecting the result to the server would be prefered (unless there are other issues that speak against this).
There are other resources on SO regarding this same issue, although all of these seem to use different libraries on the server side to do the job and nobody seems to have made any attempts at using Javascript:
Here (https://stackoverflow.com/questions/22842475/migrating-google-openid-to-openid-connect-openid-id-does-not-match) I think the problem was resolved by setting the realm to be the same as in the old OpenId2.0 flow. This does not seem to work in my case.
over here the openid_id field is also missing, but the problem here is more about how to request the id_token from Google using libraries other than DotNetOpenAuth.
and in here there seem to be similar problems getting Google to return the openid_id field.
You can use the GoogleAuthentication owin middleware.
app.UseGoogleAuthentication(new GoogleOAuth2AuthenticationOptions
{
SignInAsAuthenticationType = signAs,
AuthenticationType = "Google",
ClientId = "xxx.apps.googleusercontent.com",
ClientSecret = "xx",
CallbackPath = PathString.FromUriComponent("/oauth2callback"),
Provider = new GoogleOAuth2AuthenticationProvider
{
OnApplyRedirect = context =>
{
context.Response.Redirect(context.RedirectUri + "&openid.realm=https://mydomain.com/"); // DotNetOpenAuth by default add a trailing slash, it must be exactly the same as before
}
},
BackchannelHttpHandler = new MyWebRequestHandler()
}
Then, add a new class called MyWebRequestHandler:
public class MyWebRequestHandler : WebRequestHandler
{
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var httpResponse = await base.SendAsync(request, cancellationToken);
if (request.RequestUri == new Uri("https://www.googleapis.com/plus/v1/people/me")) return httpResponse;
var configuration = await OpenIdConnectConfigurationRetriever.GetAsync("https://accounts.google.com/.well-known/openid-configuration", cancellationToken); // read the configuration to get the signing tokens (todo should be cached or hard coded)
// google is unclear as the openid_id is not in the access_token but in the id_token
// as the middleware dot not expose the id_token we need to parse it again
var jwt = httpResponse.Content.ReadAsStringAsync().Result;
JObject response = JObject.Parse(jwt);
string idToken = response.Value<string>((object)"id_token");
JwtSecurityTokenHandler tokenHandler = new JwtSecurityTokenHandler();
try
{
SecurityToken token;
var claims = tokenHandler.ValidateToken(idToken, new TokenValidationParameters()
{
ValidAudience = "xxx.apps.googleusercontent.com",
ValidIssuer = "accounts.google.com",
IssuerSigningTokens = configuration.SigningTokens
}, out token);
var claim = claims.FindFirst("openid_id");
// claim.Value will contain the old openid identifier
if (claim != null) Debug.WriteLine(claim.Value);
}
catch (Exception ex)
{
Debug.WriteLine(ex.ToString());
}
return httpResponse;
}
}
If like me you found this not really straightforward, please help by upvoting this issue https://katanaproject.codeplex.com/workitem/359

Resources