Custom API route will respond with Forbidden if JWT token generated with Skoruba (Identity Server 4) is used - authentication

We've started implementing a new Web API with ASP.NET Core 2.2 and it has been decided that it should use Identity Server 4 for authentication/authorization duties. Furthermore, its Skoruba implementation has been chosen as it looks like it should fulfill most, if not all our needs in that regard.
We got the identity server and Skoruba up and running, but when it comes to consuming the JWT token in our own API, even assigning just one role to the test user, we'll keep hitting the same brick wall. The following request to the Skoruba API will respond with a 200:
http://localhost:5000/connect/token:
It successfully returns a JSON string with access_token, expires_in and token_type ("Bearer").
After that, a request to the http://localhost:5000/connect/userinfo route of the API with the following header
will also respond with a 200 and return the following JSON string:
{
"sub": "aeccf460-7d0d-41ae-8b52-a051138f5c05",
"role": "Administrator",
"preferred_username": "dev",
"name": "dev"
}
Please take notice that "role": "Administrator" assigned to user dev is something I set up myself using the Skoruba Admin UI, so that JSON is pretty much what I wanted. So for all intended purposes it looks like I have the information I need right now. I just can't consume it. If I try to retrieve the JWT token in our own back end, I am successful (this is obviously just for testing purposes):
[HttpGet("GetAccessToken")]
[AllowAnonymous]
public string GetAccessToken()
{
var accessToken = HttpContext.Request.Headers["Authorization"];
var token = accessToken.First().Remove(0, "Bearer ".Length);
return token;
}
With all that said, onto the actual problem: calls to a route that demands authorization in our API are treated in the same fashion as calls to Skoruba's userinfo action (particularly, the headers):
Inside this same Controller ("Foo"), I implemented a simple Get method, which should only be accessed with the correct role, which I assume is information fetched from HttpContext.Request.Headers["Authorization"] and hoped the framework would use it accordingly:
[HttpGet]
[Authorize(Roles = "Administrator")]
public IActionResult Get()
{
try
{
var response = ConvertListToJsonResponse(GetAll()); //Don't mind me
return Ok(response);
}
//...
}
At this point, my API server responds with the infamous 403 Forbidden status code. Not sure where to go from here and research proved unwieldy so far. Any help is appreciated, I'll provide more code and info if necessary.
EDIT
This is the generated token:
eyJhbGciOiJSUzI1NiIsImtpZCI6IjA4NTMzNmFmZTY0Yzg2ZWQ3NDU5YzE5YzQ4ZjQzNzI3IiwidHlwIjoiSldUIn0.eyJuYmYiOjE1Njg3NDU5NTgsImV4cCI6MTU2ODc0OTU1OCwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo1MDAwIiwiYXVkIjoiaHR0cDovL2xvY2FsaG9zdDo1MDAwL3Jlc291cmNlcyIsImNsaWVudF9pZCI6ImF1dGhfdGVzdCIsImNsaWVudF9hY2Nlc3NfcmlnaHRzIjpbImxpc3QiLCJhcHByb3ZlIiwia2VlcCJdLCJzdWIiOiJhZWNjZjQ2MC03ZDBkLTQxYWUtOGI1Mi1hMDUxMTM4ZjVjMDUiLCJhdXRoX3RpbWUiOjE1Njg3NDU5NTgsImlkcCI6ImxvY2FsIiwic2NvcGUiOlsib3BlbmlkIiwicHJvZmlsZSIsInJvbGVzIl0sImFtciI6WyJwd2QiXX0.Ihsi6W5ukGcZ4Chkuk5XMaoqTkUR_1hBQlIcdHtMWiBA-EyAIncX5STCng_6ZPgN89Np6Y_hemFFyVtHEdN_vP6i0HuaXgznzrnJ4zq4Iiz9jxpZqpSSE9cXpSG8hPOZe5kGfD2J6_GPxnraGH_1ZF94AhmlspIvqFAAQrQ-0c7-dCduP4ledkQvBKz-rXszGp35W7Gb5nvpcVt4oe67mqETdwtgGengk2eCwHeKdA94EYnj_HErPNTjJhh5k75fDQ0IiOS-xHRK8BQmLhRh_UZwB3H5qZymFJNr_yb-ljFqIeEHptSWLBO1XrKYs1BqB9KwxIROKqmxeNGTnpCUSQ
The resulting payload:
{
"nbf": 1568745958,
"exp": 1568749558,
"iss": "http://localhost:5000",
"aud": "http://localhost:5000/resources",
"client_id": "auth_test",
"client_access_rights": [
"list",
"approve",
"keep"
],
"sub": "aeccf460-7d0d-41ae-8b52-a051138f5c05",
"auth_time": 1568745958,
"idp": "local",
"scope": [
"openid",
"profile",
"roles"
],
"amr": [
"pwd"
]
}
I see the claims I added to the client, but what I really need at the moment is simple Authentication, which I suppose should be provided by a role. Or am I completely off?

This question, and more specifically, this answer helped me to understand what was going on and map Skoruba's UI functionalities to IdentityServer4 inner workings. Credits goes to Ruard van Elburg.

Related

How to inject the GraphServiceClient in Asp.net 6 (with application permissions NOT delegation permissions)

I have managed to get the GraphServiceClient to work on its own in daemon code, but would like to be able to inject it into my razor pages' constructor as well.
var queryOptions = new List<QueryOption>()
{
new QueryOption("startDateTime", "2022-08-02T16:22:00"),
new QueryOption("endDateTime", "2022-08-02T23:59:00")
};
var res = await _graphServiceClient.Users["smtp>"]
.CalendarView
.Request(queryOptions)
.Select("start,end,subject")
.GetAsync();
What code should be add to the builder?
The Id's and Secrets being read are valid and correct.
"AzureAd": {
"Instance": "https://login.microsoftonline.com/",
"Domain": "client.onmicrosoft.com",
"TenantId": "tenantid",
"ClientId": "clientid",
"ClientSecret": "clientsecret"
},
"Graph": {
"BaseUrl": "https://graph.microsoft.com/v1.0",
"Scopes": "https://graph.microsoft.com/.default"
},
I have tried the following and received: MsalUiRequiredException: No account or login hint was passed to the AcquireTokenSilent call even though it exposes an injectable GraphServiceClient to my page.
builder.Services.AddMicrosoftIdentityWebAppAuthentication(builder.Configuration, "AzureAd")
.EnableTokenAcquisitionToCallDownstreamApi()
.AddMicrosoftGraph(builder.Configuration.GetSection("Graph"))
.AddInMemoryTokenCaches();
I had the same error when I was trying to call GetAccessTokenForUserAsync (on ITokenAcquisition) at the Api Controller (Asp.net core 6.0).
"Microsoft.Identity.Client.MsalUiRequiredException: No account or login hint was passed to the AcquireTokenSilent call."
I was able to resolve it by using AuthorizeForScopes attribute on controller action. Example -
[AuthorizeForScopes(Scopes = new[] { AppConstants.ApiAppScope })]
Where AppConstants.ApiAppScope is the scope of my own WebApi.
If you are calling Graph api, it would be - https://graph.microsoft.com/.default
if you are calling your own Azure AD Web api, it would be something like api://app-id-of-web-api-as-registered-in-Azure-Ad/scope-defined-under-expose-as-api (i.e. api://ff1acf30-8c34-45ce-9bf6-7a47fa42d6be/access_as_user)
I couldn't figure out how to use built-in InMemory cache and I ended up creating my own caching. Caching is needed because call to GetAccessTokenForUserAsync will prompt the user to login again (if the scope is new).
This is my own application (self-disclaimer) that I am using for research (working sample)- https://securityresearchlab.aspnet4you.com/

Passing State and Other Attributes at Login Using Microsoft.Identity.Web

I'm having troubles getting the .NET Core 6.0 authentication libraries working as I did with prior versions of .NET Framework. Specifically, I'm using Microsoft.Identity.Web, but have not figured out how to get the scope and state parameters passed in and out that I want. The signin-oidc keeps overriding these values with the ones I am trying to manage dynamically. If I use the old method, which is just a RedirectResult to a properly formed URL for authentication, signin-oidc overrides the values I send in on the query string. If I use the Challenge class with a list of name value pairs in the Properties collection (like state, consent, scope, etc), signin-oidc still overrides them.
I would like to have something as simple, readable, and operationally efficient as when I could just redirect to a URL with all the query string parameters, and provide a callback URL that had a "code", "error", "state", and other parameters in the method. Right now I'm tied in knots over getting the right values over, and trying to get them back in a generic event override in the app's Program start up code. Some stuff shows up in a generic Properties bag, but not in the logical "State" property of the context argument's ProtocolMessage. What am I missing?
These are for cases where the user has authenticated to the site, and now I am trying to get them to consent and get a token for another Azure AD app.
Here is an example of using a simple redirect:
https://login.windows.net/organizations/oauth2/v2.0/authorize?response_type=code%20id_token&client_id=aaaaaaaa-d4d2-4499-8e2a-b6957678fe80&redirect_uri=https%3A%2F%2Flocalhost%2Ffilemonweb%2Fhome%2FProcessCode&state=my%20state%20stuff&scope=openid%20offline_access%20api%3A%2F%2F1f43d4aa-d4d2-4499-8e2a-b6957678fe80%2F.default&response_mode=form_post&nonce=aee597a2-f050-48d0-9e3c-6c62f089b76c&prompt=consent
Here's an example using Challenge:
return Challenge(new AuthenticationProperties(
new Dictionary<string, string?>() { { "state", "my state stuff" } },
new Dictionary<string, object?>() {
{ "client_id", ServiceConfig.Configuration.FILEMON_WEB_CLIENT_ID },
{ "type", "code%20id_token" },
{ "scope", "openid%20offline_access%20" + Uri.EscapeDataString(ServiceConfig.Configuration.FILEMON_WEB_SCOPE) },
{ "response_mode", "form_post" },
{ "nonce", Guid.NewGuid().ToString() },
{ "prompt", "consent"} })
{ RedirectUri = "filemonweb/home/tests" },
"OpenIdConnect");
With the Edge dev tools network sniffer, I see that the request arrives as expected with the top example (properly formatted URL). When the response is returned, I see it go to the redirect URI I requested, but the app sends a 302 response, and the response header includes a Location value that changes the redirect URI from the one I requested, to signin-oidc. For example, like this:
https://login.microsoftonline.com/organizations/oauth2/v2.0/authorize?client_id=1f43d4aa-d4d2-4499-8e2a-b6957678fe80&redirect_uri=https://localhost/filemonweb/signin-oidc&response_type=code&scope=openid profile offline_access user.read&code_challenge=ZXiKTCHjJJZwo54YdulG7h45_9e4EOBnSv_kxNOTv2w&code_challenge_method=S256&response_mode=form_post&nonce=637860882099818382.MDA0NGY1MGYtZWI0Ny00ZDU3LWJiMjQtZmE1ODk5ODdkYTEyYjQ0M2EzNjgtNzJjZi00ZjEyLWIxNTgtMGE2Y2JkYzc5YWE1&client_info=1&x-client-brkrver=IDWeb.1.16.0.0&state=CfDJ8MyJsY2aFrtOs5mEt79T88KiVV7RXqWFSO0rcSzX-NZcXlZ52qcIxpYLyz0wuWkqCh5vYPEg5Wj-YRUNMD542mvxJDGiKHz62k1hTctyvJxEtlIcZtbvLu1VOE9lNJdd6dKttBP2oi5nwDVZZC96-4bohWxxzfSk0-co8iet8xWhv8k0V2Iva1eatQ__LHOJofFQSV2IUHHmzokTB3s6reO4iLcGCyANYQWl9tp24IdQMWrwp3ZE4-DCDDQ1xzG5DZSbLAAyN29gOe5aAUwJBhmNIYX4Lm7fdsS9Bq9Xsh65h4E8Pff3U531KlDdY2WZK3gB-fyoML6rpT7DRQBN1Z5ls686pyMxtQRVN-LcQEYXCsmv7WZF3yiSQ-ctIN3X1GOehgTPJrSxpb8LxoT-Z9Bo_lQLEmwOvXw-9qbDzntv&x-client-SKU=ID_NETSTANDARD2_0&x-client-ver=6.12.1.0
When it sends that 302 response, it changes the State parameter, the Scope parameter, etc.
Thanks.

.Net core api with AD B2C OAuth 2.0 - Invalid_token error

I am following this resource. I can get the token successfully but get 401 upon using the token in the second call to my api. It says Bearer error='invalid_token'. Earlier it was giving "Invalid issuer" so I decoded the token to use the issuer in "Instance" field of appSettings.json. Following are appSettings and token. What am I doing wrong?
appSettings.json
{
"AzureAdB2C": {
"Instance": "https://login.microsoftonline.com/xxxxxxxxxxxxxxxxxxxxx/v2.0/",
"ClientId": "452gfsgsdfgsdgssfs5425234",
"Domain": "xxxxxxxxxxxxxxx.onmicrosoft.com",
"SignUpSignInPolicyId": "B2C_1_Auth-SignUpIn"
},
"Logging": {
"LogLevel": {
"Default": "Warning"
}
},
"AllowedHosts": "*"
}
token
{
"iss": "https://login.microsoftonline.com/23423fsf234234sfsd42342vsx2542/v2.0/",
"exp": 1551878022,
"nbf": 1551874422,
"aud": "ee965664-d1e3-4144-939a-11f77c523b50",
"oid": "a9ee8ebb-433d-424b-ae24-48c73ae9969c",
"sub": "a9ee8ebb-433d-424b-ae24-48c73ae9969c",
"name": "unknown",
"extension_xxxID": "9f27fd88-7faf-e411-80e6-005056851bfe",
"emails": [
"dfgdfgadfgadfg#dgadg.com"
],
"tfp": "B2C_1_Auth-SignUpIn",
"scp": "user_impersonation",
"azp": "4453gdfgdf53535bddhdh",
"ver": "1.0",
"iat": 1551874422
}
AD B2C instance
Azure AD B2C setting
Postman - revalapi highlighted is the uri of the registered app in the previous shot
Token
Error
Ok. Looks like AD B2C + .Net Core is not happy with onmicrosoft.com URI even though the Microsoft docs resource say it does. See here. I had to use the b2clogin.com uri as shown in below screen shots. Hope it helps someone.
Postman
AppSettings.json
Startup.Auth.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(AzureADB2CDefaults.BearerAuthenticationScheme)
.AddAzureADB2CBearer(options => Configuration.Bind("AzureAdB2C", options));
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
services.AddApplicationInsightsTelemetry();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseHsts();
}
app.UseAuthentication();
app.UseHttpsRedirection();
app.UseMvc();
}
For registering your B2C dotnet core application first You have to login to your B2C Tenant.
After successful Registration configure following step for implicit grant flow.
Reply URLs
Make sure you have done this step accordingly:
Go to Settings and add postman callback URL to : https://www.getpostman.com/oauth2/callback
Once you enter this URL correctly click on Save upper left.
See the screen shot below:
Edit Manifest
For implicit grand flow click on your application manifest and search oauth2AllowImplicitFlow property
make it to true
see the screen shot below:
Your settings for azure B2C is done for implicit grant flow API call.
Postman
Now fire up your post man and select request type as OAuth 2.0 Like below:
Now Click on Get New Access Token and new popup will appear
See the screen shot below:
Add your tenant ID on Auth URL Like this :
https://login.microsoftonline.com/YourB2CTenantId/oauth2/authorize?resource=https://graph.microsoft.com
Set Your Client Id
Set Scope you want to access
Now click on Request Token In response you will get your implicit grant access token:
see the screen shot:
Access Data With this Token:
Copy the token you have accessed already on the Token textbox and select token type as Bearer Token
See the screen shot below:
So tricky part for implicit flow is to set up manifest property oauth2AllowImplicitFlow to true
Hope this could solve your problem. Thank you

Spring security WSO2 IS integration - Figure out how to assign authorities/customize wso2 token

I am very stuck with this issue for a couple of days.
So what I am trying to do is to assign ROLES with spring security framework. My goal is to decode token that I get from WSO2 Identity Server 5.0 through openid and assign the Role so I can authorize the request based on Roles (AUTHORITIES)
This is my SecurityConfig class in simple spring boot app
#Profile("oauth")
#Configuration
#EnableResourceServer
public class SecurityConfig {
}
So, with this configuration, I am able to decode the token.
However, in debug mode, when I made a request with the id_token to the simple spring boot app, I received an error:
java.lang.ClassCastException
java.lang.String cannot be cast to java.util.Collection
And it happens in DefaultAccessTokenConverter class, particularly in the line of code when the map object is converted to String [] roles
public OAuth2Authentication extractAuthentication(Map<String, ?> map) {
...
if (user==null && map.containsKey(AUTHORITIES)) {
#SuppressWarnings("unchecked")
String[] roles = ((Collection<String>)map.get(AUTHORITIES)).toArray(new String[0]);
authorities = AuthorityUtils.createAuthorityList(roles);
}
OAuth2Request request = new OAuth2Request(parameters, clientId, authorities, true, scope, resourceIds, null, null,
null);
return new OAuth2Authentication(request, user);
}
This is my WSO2 decoded token
{
"auth_time": 1464819792,
"exp": 1464823490,
"azp": "U1PXsuyV_tdBERmZIoHHnqoGkWIa",
"authorities": "[\"ROLE_ADMIN\",\"approver\",\"Internal\/everyone\"]",
"at_hash": "Hh2LUZl3Bp6yDqyZt4r6Gg",
"aud": [
"U1PXsuyV_tdBERmZIoHHnqoGkWIa"
],
"iss": "https://localhost:9443/oauth2/token",
"locality": "[\"ROLE_ADMIN\"]",
"iat": 1464819890
}
It seems that Spring expects Array of String, not String object (there is a double quote at the beginning and the end of value in authorities.
The aud format seems to be what the spring expects.
So, there are two options I can think o
1. Write some configuration in Spring Oauth2 (I have not figured this out yet)
2. Configure WSO2 Identity Server (This what I've been trying to do).
There are some resources saying that we can implement our own JWTTokenGenerator in WSO2 carbon. From looking at the code, it seems this is where the double quotes are generated in the claim.
org.wso2.carbon.identity.oauth2.authcontext.JWTTokenGenerator
I hope there is someone else who has been going through this.
Thank you very much.
Please find the default implementation here [1]. Also it is better if you can go with IS 5.1.0 for 5.1.0 refer [2]. After building custom JWTTokenGenerator copy it to repository/components/lib. Change
<TokenGeneratorImplClass>
element in identity.xml according to your custom implementation.
[1] https://svn.wso2.org/repos/wso2/carbon/platform/branches/turing/components/identity/org.wso2.carbon.identity.oauth/4.2.3/src/main/java/org/wso2/carbon/identity/oauth2/authcontext/JWTTokenGenerator
[2]https://github.com/wso2/carbon-identity/tree/master/components/oauth/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/authcontext
Thank you! That could work too! but for easier implementation, we use 5.2.0 beta version that that produce array of string. T

Google OAuth2 Service Accounts API Authorization

I'm trying to authenticate my server app through Google's service account authentication but, for some reason, it is just not pushing through.
In the API console, I already created the project, enabled the service I need (Admin SDK), and created a Service Account and Web Application API Access.
When I do use the web application access credentials I am able to authenticate and retrieve user records. But using service account authentication would keep giving me a login required message.
"error": { "errors": [ { "domain": "global", "reason": "required", "message": "Login Required", "locationType": "header", "location": "Authorization" } ], "code": 401, "message": "Login Required" }
I forgot to add, I am testing this with the PHP client library.
public function init() {
$client = new Google_Client();
if (isset($_SESSION['access_token'])) {
$client->setAccessToken($_SESSION['access_token']);
}
$key = file_get_contents(App::SERVICE_KEY_FILE);
$client->setAssertionCredentials(new Google_AssertionCredentials(
App::SERVICE_ACCOUNT_NAME,
App::SERVICE_API_SCOPES,
$key)
);
$client->setClientId(App::SERVICE_CLIENT_ID);
debug($client, 'CLIENT');
if ($client->getAccessToken()) {
$this->access_token = $_SESSION['access_token'] = $client->getAccessToken();
debug($_SESSION['access_token'], 'TOKEN');
} else {
debug('NO TOKEN');
}
$this->client = $client;
}
As you can see, the code is basically about the same as the Google example. Am I missing an extra step?
One last thing, when I authenticate using the web app then access my service account script, the service account script can pick up the web app script's session and push through with the user record retrievals. Does that mean the Admin SDK API explicitly needs user interaction through web app authentication?
Instead of service account, I instead opted to use installed applications API Access.
This ruby gem actually helped my figure this out - https://github.com/evendis/gmail_cli
I was playing with it on the console and just followed the authorization steps in the readme, and found that installed applications is more simple when doing server admin apps.
Being a newb, I think I missed the important part the refresh token plays in the entire process. Going via the installed application approach helped me figure that out.
My config file now contains the client id, client secret, api scope, redirect uri, authorization code, and the refresh token; my initialization code now looks like:
public function init() {
$client = new Google_Client();
$client->setClientId(App::CLIENT_ID);
$client->setClientSecret(App::CLIENT_SECRET);
$client->setScopes(App::API_SCOPES);
$client->setRedirectUri(App::REDIRECT_URI);
if (!$client->getAccessToken()) {
$client->refreshToken(App::REFRESH_TOKEN);
}
$this->access_token = $client->getAccessToken();
$this->client = $client;
}