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?
Related
I have managed to establish a login screen using Javascript google API as described in https://developers.google.com/identity/sign-in/web/server-side-flow. using the function: auth2.grantOfflineAccess()
The API returns the following authorization code:
{"code":"4/yU4cQZTMnnMtetyFcIWNItG32eKxxxgXXX-Z4yyJJJo.4qHskT-UtugceFc0ZRONyF4z7U4UmAI"}
How do I exchange the authorization code to access token and a refresh token in an ASP.NET server?
Basically you need to post it back to google to get an access token and a refresh token.
POST https://accounts.google.com/o/oauth2/token
code={CODE}&client_id={ClientId}&client_secret={ClientSecret}&redirect_uri=urn:ietf:wg:oauth:2.0:oob&grant_type=authorization_code
Here is a server-side code for for google sign in:
UserCredential credential;
string[] scopes = new string[] {
YouTubeService.Scope.YoutubeUpload
};
IAuthorizationCodeFlow flow = new GoogleAuthorizationCodeFlow(new GoogleAuthorizationCodeFlow.Initializer
{
ClientSecrets = new ClientSecrets
{
ClientId = "<CLIENT-ID>",
ClientSecret = "<CLIENT-SECRET>"
},
Scopes = scopes,
DataStore = new FileDataStore("Store")
});
TokenResponse token = flow.ExchangeCodeForTokenAsync(videoUploadOptions.userId, videoUploadOptions.authorizationCode, videoUploadOptions.baseUrl, CancellationToken.None).Result;
credential = new UserCredential(flow, Environment.UserName, token);
The auth2 google response will be stored at the token object. the variable transferred from the client are: videoUploadOptions.userId, videoUploadOptions.authorizationCode and videoUploadOptions.baseUrl.
All the credentials will be stored at the credential object.
I am developing a C# Web Api (.NET Framework) and would like to use in parallel the AAD authentication (already working correctly) and Google Authentication. This means, my clients (javascript or just Postman) should fetch the token, include it in the Authorization header (Bearer token) and be able to execute the API methods.
However, it seems that the token I am generating with Postman is not accepted by my C# Web Api. I am always getting a HTTP 401. The AAD (Windows Azure Active Directory) works seamlessly.
Using a C# Web Api (.Net Framework 4.6.1), including Microsoft.Owin.Security.Google Nuget package v.4.0.1.
I have created the Oauth client on the google developer console, got my client ID and client secret. Also I have set up the redirect URI there.
I am using Postman Oauth2 authorization, setting following parameters:
Grant type: implicit
Callback URL: my url
AUth URL: https://accounts.google.com/o/oauth2/v2/auth
Client ID: 65xxxxxmyclientid.apps.googleusercontent.com
scope: openid email profile
Then I can log in with my google account, give consent to use the scopes, and am getting the token like this:
Access Token
ya29.Gls7....
This token is then sent as Authorization header like "Bearer ya29.Gls7...."
Startup.Auth
public void ConfigureAuth(IAppBuilder app)
{
app.UseWindowsAzureActiveDirectoryBearerAuthentication(
new WindowsAzureActiveDirectoryBearerAuthenticationOptions
{
Tenant = ConfigurationManager.AppSettings["ida:Tenant"],
TokenValidationParameters = new TokenValidationParameters
{
ValidAudience = ConfigurationManager.AppSettings["ida:Audience"]
},
});
app.UseGoogleAuthentication(new GoogleOAuth2AuthenticationOptions()
{
ClientId = ConfigurationManager.AppSettings["GoogleClientId"],
ClientSecret = ConfigurationManager.AppSettings["GoogleClientSecret"],
});
}
ValuesController
public class ValuesController : ApiController
{
[Authorize]
// GET api/values/5
public string Get(int id)
{
return "value";
}
}
Every time I call the API using Postman, I am getting a 401, even though the bearer token is included in the request authorization header.
HTTP GET https://localhost:44385/api/values/1
Edit: There is no custom code from my side which would validate the token. I assumed that is being done automatically by the library. In fact, this is done automatically for AAD tokens, but not in the case of Google Oauth.
I expect to get inside the code of the controller method, having at least the identity name set with the google account name/email.
What am I missing to be authenticated inside my controller method with google account?
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 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
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.