Separating Auth and Resource Servers with AspNet.Security.OpenIdConnect - the Audience? - asp.net-web-api

The example on the AspNet.Security.OpenIdConnect.Server looks to me like both an auth and resource server. I would like to separate those. I have done so.
At the auth server's Startup.Config, I have the following settings:
app.UseOpenIdConnectServer(options => {
options.AllowInsecureHttp = true;
options.ApplicationCanDisplayErrors = true;
options.AuthenticationScheme = OpenIdConnectDefaults.AuthenticationScheme;
options.Issuer = new System.Uri("http://localhost:61854"); // This auth server
options.Provider = new AuthorizationProvider();
options.TokenEndpointPath = new PathString("/token");
options.UseCertificate(new X509Certificate2(env.ApplicationBasePath + "\\mycertificate.pfx","mycertificate"));
});
I have an AuthorizationProvider written, but I don't think it's relevant to my current issue (but possibly relevant). At its GrantResourceOwnerCredentials override, I hard-code a claims principal so that it validates for every token request:
public override Task GrantResourceOwnerCredentials(GrantResourceOwnerCredentialsNotification context)
{
var identity = new ClaimsIdentity(OpenIdConnectDefaults.AuthenticationScheme);
identity.AddClaim(ClaimTypes.Name, "me");
identity.AddClaim(ClaimTypes.Email, "me#gmail.com");
var claimsPrincipal = new ClaimsPrincipal(identity);
context.Validated(claimsPrincipal);
return Task.FromResult<object>(null);
}
At the resource server, I have the following in its Startup.config:
app.UseWhen(context => context.Request.Path.StartsWithSegments(new PathString("/api")), branch =>
{
branch.UseOAuthBearerAuthentication(options => {
options.Audience = "http://localhost:54408"; // This resource server, I believe.
options.Authority = "http://localhost:61854"; // The auth server
options.AutomaticAuthentication = true;
});
});
On Fiddler, I ask for a token, and I get one:
POST /token HTTP/1.1
Host: localhost:61854
Content-Type: application/x-www-form-urlencoded
username=admin&password=aaa000&grant_type=password
So now I use that access token to access a protected resource from the resource server:
GET /api/values HTTP/1.1
Host: localhost:54408
Content-Type: application/json;charset=utf-8
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI.....
I now get this error - Audience validation failed. Audiences: 'empty'. Did not match validationParameters.ValidAudience: 'http://localhost:54408' or validationParameters.ValidAudiences: 'null'.
I think the reason why is because I never set an audience at the auth server (at app.UseOpenIdConnectServer(...)), so I don't think it wrote audience info to the token. So I need to set an audience at the auth server (as what is done in IdentityServer3), but I can't find a property on the options object that would let me do that.
Does AspNet.Security.OpenIdConnect.Server require the auth and resource to be in the same server?
Is setting the audience done when putting together the ClaimsPrincipal, and if so, how?
Would I need to write a custom Audience validator and hook it up to the system? (I sure hope the answer to this is no.)

Does AspNet.Security.OpenIdConnect.Server require the auth and resource to be in the same server?
No, you can of course separate the two roles.
As you've already figured out, if you don't explicitly specify it, the authorization server has no way to determine the destination/audience of an access token, which is issued without the aud claim required by default by the OAuth2 bearer middleware.
Solving this issue is easy: just call ticket.SetResources(resources) when creating the authentication ticket and the authorization server will know exactly which value(s) (i.e resource servers/API) it should add in the aud claim(s).
app.UseOpenIdConnectServer(options =>
{
// Force the OpenID Connect server middleware to use JWT tokens
// instead of the default opaque/encrypted token format used by default.
options.AccessTokenHandler = new JwtSecurityTokenHandler();
});
public override Task HandleTokenRequest(HandleTokenRequestContext context)
{
if (context.Request.IsPasswordGrantType())
{
var identity = new ClaimsIdentity(context.Options.AuthenticationScheme);
identity.AddClaim(OpenIdConnectConstants.Claims.Subject, "unique identifier");
var ticket = new AuthenticationTicket(
new ClaimsPrincipal(identity),
new AuthenticationProperties(),
context.Options.AuthenticationScheme);
// Call SetResources with the list of resource servers
// the access token should be issued for.
ticket.SetResources("resource_server_1");
// Call SetScopes with the list of scopes you want to grant.
ticket.SetScopes("profile", "offline_access");
context.Validate(ticket);
}
return Task.FromResult(0);
}
app.UseJwtBearerAuthentication(new JwtBearerOptions
{
AutomaticAuthenticate = true,
AutomaticChallenge = true,
Audience = "resource_server_1",
Authority = "http://localhost:61854"
});

Related

AAD Token from SPA app works to call Nodejs API but not .Net WebAPI

I have a SPA application that uses MSAL to acquire a token from AAD. Because MSAL works with v2 endpoint and because v2 endpoint does not currently support issuing tokens for custom API's, I'm passing the ID Token to my api and essentially treating my api as the same application. (While this has a smell to it, it does actually work -- at least with Nodejs API).
SPA app
let idToken = Msal.Storage('localStorage').getItem(Msal.Constants.idTokenKey);
this.http.configure(config => {
config.withBaseUrl("http://localhost:3001/")
config.withDefaults({headers: {'Authorization': 'Bearer ' + idToken}})
});
//Call API
this.http.fetch("account")
...
Node.js API
//Using express/passport
var BearerStrategy = require("passport-azure-ad").BearerStrategy;
var options = {
identityMetadata: "https://login.microsoftonline.com/tenantid/.well-known/openid-configuration/",
clientID: "xxxxxxx-xxxx-xxxxxxx-xxxxx",
passReqtoCallback: false,
validateIssuer: true,
issuer: "http://login.microsoftonline.com/{tenantid}/v2.0"
};
app.get("/account",passport.authenticate('oauth-bearer',{session: false}),...
The above all works. Once a user authenticates with the SPA, the token is passed and the call to the Node API works.
I'm now trying to replace the Nodejs API with a .Net WebAPI. I have the following:
Startup.cs
app.UseWindowsAzureActiveDirectoryBearerAuthentication(
new WindowsAzureActiveDirectoryBearerAuthenticationOptions
{
TokenValidationParameters = new TokenValidationParameters
{
//Same ID as used for ClientID in Nodejs
ValidAudience = "xxxxxx-xxxxx-xxxxx-xxxxx",
ValidIssuer = "https://login.microsoftonline.com/{tenantid}/v2.0",
ValidateIssuer = true,
AuthenticationType = "WebApi" //Tried both with and without this
},
Tenant = "{tenantid}" //have tried both id and name
}
)
AccountController.cs
[Authorize]
[Route("account")]
public IHttpActionResult AccountProfile(){
//Get Account information
....
return Ok(profile);
}
However, when I point the SPA app to call the .Net api, I always get Authorization has been denied for this request .
Is there something I'm missing?
Edit
Incidentally, I've inspected the token that is being used.
The value I'm using for clientID (Nodejs) and ValidAudience (.Net) exactly match the aud claim in the token. The issuer (Nodejs) and ValidIssuer (.Net) exactly match the iss claim in the token. Lastly, the anywhere in my code where I've inserted {tenantid}, the actual value there matches exactly the tid claim in the token.
We had a similar issue when switching from ADAL to MSAL and got it to work by using a similar approach like this Github project. Specifically take a look at these files:
https://github.com/oktadeveloper/okta-oauth-aspnet-codeflow/blob/master/Api/Startup.cs
https://github.com/oktadeveloper/okta-oauth-aspnet-codeflow/blob/master/Api/OpenIdConnectCachingSecurityTokenProvider.cs
Update: Our Startup.cs:
var provider = new OpenIdConnectCachingSecurityTokenProvider(
string.Format(bc2Instace, tenant, policyId));
var jwt = new JwtFormat(clientId, provider);
app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions
{
AccessTokenFormat = jwt,
});

How to know that access token has expired?

How should client know that access token has expired, so that he makes a request with refresh token for another access token?
If answer is that server API will return 401, then how can API know that access token has expired?
I'm using IdentityServer4.
Your api should reject any call if the containing bearer token has already been expired. For a webapi app, IdentityServerAuthenticationOptions will do the work.
But your caller Web application is responsible for keeping your access_token alive. For example, if your web application is an ASP.Net core application, you may use AspNetCore.Authentication.Cookies to authenticate any request. In that case, you can find the information about the token expiring info through OnValidatePrincipal event.
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationScheme = "Cookies",
//ExpireTimeSpan = TimeSpan.FromSeconds(100),
AutomaticAuthenticate = true,
AutomaticChallenge = true,
Events = new CookieAuthenticationEvents()
{
OnValidatePrincipal = async x =>
{
if (x.Properties?.Items[".Token.expires_at"] == null) return;
var now = DateTimeOffset.UtcNow;
var tokenExpireTime = DateTime.Parse(x.Properties.Items[".Token.expires_at"]).ToUniversalTime();
var timeElapsed = now.Subtract(x.Properties.IssuedUtc.Value);
var timeRemaining = tokenExpireTime.Subtract(now.DateTime);
if (timeElapsed > timeRemaining)
{
//Get the new token Refresh the token
}
}
}
}
I have added a full implementation about how to get a new access token using refresh token in another StackOverflow answer

Laravel Passport - get access token in controller by user id?

I was wondering if it was possible to get access_token by user's id. I'm using nodejs server to push notifications to client, and I need to validate connected user - curious if I could do that by using the same 'Bearer...' access token sent to user by oauth2.
As a solution I'm feeding token from controller to node server, and after client connects to node server the client is requested to send bearer token - and extracting jti from it using jsonwebtoken package, which is used to compare with token received from controller.
Here goes the code:
Controller
$userTokenReceiver = $pm->receiver->tokens()->where('revoked', 'false')
->select('id')->get()->sortBy('created_at', true)->first();
if ($userTokenReceiver && $userTokenReceiver->toArray()['id'])
$dataRedis->receiver_token = $userTokenReceiver->toArray()['id'];
Node server
var jwt = require('jsonwebtoken');
...
io.on('connection', function(socket) {
socket.waitingAuthorize = true;
// E> authentication_request: after connection asking client to provide token
socket.emit('authentication_request');
// O> authorize: waiting for client's response to auth request
socket.on('authorize', function(token) {
var decoded = jwt.decode(token);
if ( !decoded || !decoded.jti ) {
socket.disconnect(true);
return;
}
socket.waitingAuthorize = false;
socket.token = decoded.jti; // this is compared to token received from controller
});
...

Asp.net Core Client with Identityserver 3 missing claims

Given:
A JavaScript app that authenticates with oidc over identityerver v3
A Asp.net Core Webapi that authenticates with the given bearer token to identiyserver
The Javascript clients makes calls with the access token itself to the api.
Problem:
The Authentication suceeds but the restored principal is missing some custom claim like "username" and "familyName". I can see that the oidc client in the javascript client has these informations
some claims like "idp" is set in both Javascript and Api Client. But bot are not handled explicitly.
The main difference is that idp is part of the access_token which the username is not.
the configuration of the api is :
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
var authority = config["identity:authority:url"];
app.UseIdentityServerAuthentication(new IdentityServerAuthenticationOptions
{
LegacyAudienceValidation = true,
Authority = authority,
RequireHttpsMetadata = false,
EnableCaching = false,
ApiName = "MyApp.Read",
});
Any hint what i'm missing ? ( I assume it is some kind of profile read in the api?)
Workaround
I extend the configuration with JwtBearerEvents and make a manual read with userclient when the token was authenticated like this
JwtBearerEvents = new JwtBearerEvents
{
OnTokenValidated = async context =>
{
string header = context.Request.Headers["Authorization"];
string accessToken = header.Substring(6);
var result = await userInfoClient.GetAsync(accessToken);
but is this the intended way? Are extended / profile claims meant to be returned only by manually querying them?

google ExchangeCodeForTokenAsync invalid_grant in webapi

i have implemented GoogleAuthorizationCodeFlow scenario from google api client dotnet and tutorial to get token from what my client sent to server as a code. but when i call flow.ExchangeCodeForTokenAsync , I get the following error :
{"Error:\"invalid_grant\", Description:\"\", Uri:\"\""}
I read google authorization invalid_grant and gusclass oauth 2 using google dotnet api client libraries but they didn't help me and. I think it must be very simple but I don't know why it doesn't work.
For client side , I have used Satellizer and this is my server Codes:
public bool PostExchangeAccessToken(GoogleClientAccessCode code)
{
string[] SCOPES = { "email" };
IAuthorizationCodeFlow flow = new GoogleAuthorizationCodeFlow(new GoogleAuthorizationCodeFlow.Initializer
{
ClientSecrets = new ClientSecrets()
{
ClientSecret = "******",
ClientId = "********.apps.googleusercontent.com"
},
Scopes = SCOPES
});
try
{
TokenResponse token;
token = flow.ExchangeCodeForTokenAsync("*****#gmail.com", Newtonsoft.Json.JsonConvert.SerializeObject(code), "https://localhost:44301/",
CancellationToken.None).Result;
}
catch (Exception ex)
{
throw ex;
}
return true;
}
what is the problem?
On Github I found that I must use the Token from the client and use
GoogleAuthorizationCodeFlow.Initializer()
to create my UserCredential object.
You can check your google developer console settings.(Authorized redirect URIs)
Credentials => OAuth 2.0 client IDs => Your Application Settings => Authorized redirect URIs
You must add url. ("https://localhost:44301/")
My code :
flow.ExchangeCodeForTokenAsync("me", authCode, redirectUri, CancellationToken.None).Result;
Authorized redirect URIs
For use with requests from a web server. This is the path in your application that users are redirected to after they have authenticated with Google. The path will be appended with the authorization code for access. Must have a protocol. Cannot contain URL fragments or relative paths. Cannot be a public IP address.

Resources