How to access external provider's access token at OnRedirectToIdentityProviderForSignOut of OpenIdConnectEvents with IdentityServer4? - identityserver4

My application need to integration with a custom Identity provider that does not support end_session_endpoint.
However it does support SLO by using the URL such as
https://idp.example.com/logout?token={access_token_or_refresh_token}&goto={post_logout_redirect_url}
I have figured to setup the custom URL through
options.Events = new OpenIdConnectEvents
{
OnRedirectToIdentityProviderForSignOut = context =>
{
context.ProtocolMessage.IssuerAddress = "https://idp.example.com/logout";
context.ProtocolMessage.SetParameter("goto", context.ProtocolMessage.PostLogoutRedirectUri);
return Task.CompletedTask;
}
};
But I could not figure out how to gain access to the access token of the external provider and set it through SetParameter.
Any help will be appreciated. Thanks.
I have tried setting breakpoint at the event and there is not much information other than id_token_hint in the parameters.

Related

Is there a way to avoid using the redirected form in Spring OAuth2 Authorization server when trying to get Authorization code? [duplicate]

I'm trying to create a local Java-based client that interacts with the SurveyMonkey API.
SurveyMonkey requires a long-lived access token using OAuth 2.0, which I'm not very familiar with.
I've been googling this for hours, and I think the answer is no, but I just want to be sure:
Is it possible for me to write a simple Java client that interacts with the SurveyMonkey, without setting up my own redirect server in some cloud?
I feel like having my own online service is mandatory to be able to receive the bearer tokens generated by OAuth 2.0. Is it possible that I can't have SurveyMonkey send bearer tokens directly to my client?
And if I were to set up my own custom Servlet somewhere, and use it as a redirect_uri, then the correct flow would be as follows:
Java-client request bearer token from SurveyMonkey, with
redirect_uri being my own custom servlet URL.
SurveyMonkey sends token to my custom servlet URL.
Java-client polls custom servlet URL until a token is available?
Is this correct?
Yes, it is possible to use OAuth2 without a callback URL.
The RFC6749 introduces several flows. The Implicit and Authorization Code grant types require a redirect URI. However the Resource Owner Password Credentials grant type does not.
Since RFC6749, other specifications have been issued that do not require any redirect URI:
RFC7522: Security Assertion Markup Language (SAML) 2.0 Profile for OAuth 2.0 Client Authentication and Authorization Grants
RFC7523: JSON Web Token (JWT) Profile for OAuth 2.0 Client Authentication and Authorization Grants
RFC8628: OAuth 2.0 Device Authorization Grant
In any case, if the grant types above do not fit on your needs, nothing prevent you from creating a custom grant type.
Not exactly, the whole point of the OAuth flow is that the user (the client you're accessing the data on behalf of) needs to give you permission to access their data.
See the authentication instructions. You need to send the user to the OAuth authorize page:
https://api.surveymonkey.net/oauth/authorize?api_key<your_key>&client_id=<your_client_id>&response_type=code&redirect_uri=<your_redirect_uri>
This will show a page to the user telling them which parts of their account you are requesting access to (ex. see their surveys, see their responses, etc). Once the user approves that by clicking "Authorize" on that page, SurveyMonkey will automatically go to whatever you set as your redirect URI (make sure the one from the url above matches with what you set in the settings for your app) with the code.
So if your redirect URL was https://example.com/surveymonkey/oauth, SurveyMonkey will redirect the user to that URL with a code:
https://example.com/surveymonkey/oauth?code=<auth_code>
You need to take that code and then exchange it for an access token by doing a POST request to https://api.surveymonkey.net/oauth/token?api_key=<your_api_key> with the following post params:
client_secret=<your_secret>
code=<auth_code_you_just_got>
redirect_uri=<same_redirect_uri_as_before>
grant_type=authorization_code
This will return an access token, you can then use that access token to access data on the user's account. You don't give the access token to the user it's for you to use to access the user's account. No need for polling or anything.
If you're just accessing your own account, you can use the access token provided in the settings page of your app. Otherwise there's no way to get an access token for a user without setting up your own redirect server (unless all the users are in the same group as you, i.e. multiple users under the same account; but I won't get into that). SurveyMonkey needs a place to send you the code once the user authorizes, you can't just request one.
You do need to implement something that will act as the redirect_uri, which does not necessarily need to be hosted somewhere else than your client (as you say, in some cloud).
I am not very familiar with Java and Servelets, but if I assume correctly, it would be something that could handle http://localhost:some_port. In that case, the flow that you describe is correct.
I implemented the same flow successfully in C#. Here is the class that implements that flow. I hope it helps.
class OAuth2Negotiator
{
private HttpListener _listener = null;
private string _accessToken = null;
private string _errorResult = null;
private string _apiKey = null;
private string _clientSecret = null;
private string _redirectUri = null;
public OAuth2Negotiator(string apiKey, string address, string clientSecret)
{
_apiKey = apiKey;
_redirectUri = address.TrimEnd('/');
_clientSecret = clientSecret;
_listener = new HttpListener();
_listener.Prefixes.Add(address + "/");
_listener.AuthenticationSchemes = AuthenticationSchemes.Anonymous;
}
public string GetToken()
{
var url = string.Format(#"https://api.surveymonkey.net/oauth/authorize?redirect_uri={0}&client_id=sm_sunsoftdemo&response_type=code&api_key=svtx8maxmjmqavpavdd5sg5p",
HttpUtility.UrlEncode(#"http://localhost:60403"));
System.Diagnostics.Process.Start(url);
_listener.Start();
AsyncContext.Run(() => ListenLoop(_listener));
_listener.Stop();
if (!string.IsNullOrEmpty(_errorResult))
throw new Exception(_errorResult);
return _accessToken;
}
private async void ListenLoop(HttpListener listener)
{
while (true)
{
var context = await listener.GetContextAsync();
var query = context.Request.QueryString;
if (context.Request.Url.ToString().EndsWith("favicon.ico"))
{
context.Response.StatusCode = (int)HttpStatusCode.NotFound;
context.Response.Close();
}
else if (query != null && query.Count > 0)
{
if (!string.IsNullOrEmpty(query["code"]))
{
_accessToken = await SendCodeAsync(query["code"]);
break;
}
else if (!string.IsNullOrEmpty(query["error"]))
{
_errorResult = string.Format("{0}: {1}", query["error"], query["error_description"]);
break;
}
}
}
}
private async Task<string> SendCodeAsync(string code)
{
var GrantType = "authorization_code";
//client_secret, code, redirect_uri and grant_type. The grant type must be set to “authorization_code”
var client = new HttpClient();
client.BaseAddress = new Uri("https://api.surveymonkey.net");
var request = new HttpRequestMessage(HttpMethod.Post, string.Format("/oauth/token?api_key={0}", _apiKey));
var formData = new List<KeyValuePair<string, string>>();
formData.Add(new KeyValuePair<string, string>("client_secret", _clientSecret));
formData.Add(new KeyValuePair<string, string>("code", code));
formData.Add(new KeyValuePair<string, string>("redirect_uri", _redirectUri));
formData.Add(new KeyValuePair<string, string>("grant_type", GrantType));
formData.Add(new KeyValuePair<string, string>("client_id", "sm_sunsoftdemo"));
request.Content = new FormUrlEncodedContent(formData);
var response = await client.SendAsync(request);
if (!response.IsSuccessStatusCode)
{
_errorResult = string.Format("Status {0}: {1}", response.StatusCode.ToString(), response.ReasonPhrase.ToString());
return null;
}
var data = await response.Content.ReadAsStringAsync();
if (data == null)
return null;
Dictionary<string, string> tokenInfo = JsonConvert.DeserializeObject<Dictionary<string, string>>(data);
return(tokenInfo["access_token"]);
}
}

Generate an access token in the browser that is scoped to Kusto Azure Data Explorer

From the command line I can generate an access token using:
az account get-access-token
However this access token won't work with kusto because the token has not been scoped.
az account get-access-token --resource https://{name}.{location}.kusto.windows.net
This provides an access token that is accepted by kusto.
My question relates to the #azure/msal-browser library and how I can scope the access token a specific resource. In the example below I am using #azure/msal-react which is just an abstraction layer on top of the underlying #azure/msal-browser library.
import { useMsal, useAccount } from '#azure/msal-react';
const ControllerRequester = () => {
const { accounts, instance } = useMsal();
const account = useAccount(accounts[0] || {});
if (Object.keys(accounts).length) {
instance.acquireSilentToken({
account,
// What other properties do I need to include to scope the access token to kusto
}).then(accessToken => doSomething(accessToken))
}
return <div>...</div>
}
I've tried a few different options, such as scopes: ['https://{name}.{location}.kusto.windows.net/.default'] but without much success. I've also read other answers relating to the msal library but they haven't helped. Any help would be appreciated!
it looks like currently this is used https://{name}.{location}.kusto.windows.net/.default
Try removing the .default

Azure AD v2.0-specific optional claims missing from ID Token

I'm trying to add optional claims using Microsoft Identity Web - NuGet for user authentication in NET Core 3.1 WebApp. Reading the MS Docs, it seems that the only steps needed are to declare the optional claims within the App Registration Manifest file in Azure. But when testing the login process using two different apps (my own code and an MS project example) it looks like the optional claims are not being added to the ID Token when returned from Azure following a successful login i.e they're not present at all when viweing the token details in Debug.
I'm not sure how to diagnose this and where to trace the issue i.e am I missing any required steps in Azure setup?
Side Note: Just to confirm it is the jwt ID Token I want to receive the additional claims, NOT the jwt access token used for calling the graph or another Web API endpoint.
MS Docs reference: v2.0-specific optional claims set
Below is the extract from the Manifest file: (note I've even declared the "accessTokenAcceptedVersion": 2, given that optional claims I'm using are not available in ver.1, which if the above was left at default 'null' value then Azure will assume we're using legacy ver.1 - a possible gotcha)
"accessTokenAcceptedVersion": 2,
"optionalClaims": {
"idToken": [
{
"name": "given_name",
"source": "user",
"essential": false,
"additionalProperties": []
},
{
"name": "family_name",
"source": "user",
"essential": false,
"additionalProperties": []
}
],
"accessToken": [],
"saml2Token": []
},
Extract from startup class:
public void ConfigureServices(IServiceCollection services)
{
// Added to original .net core template.
// ASP.NET Core apps access the HttpContext through the IHttpContextAccessor interface and
// its default implementation HttpContextAccessor. It's only necessary to use IHttpContextAccessor
// when you need access to the HttpContext inside a service.
// Example usage - we're using this to retrieve the details of the currrently logged in user in page model actions.
services.AddHttpContextAccessor();
// DO NOT DELETE (for now...)
// This 'Microsoft.AspNetCore.Authentication.AzureAD.UI' library was originally used for Azure Ad authentication
// before we implemented the newer Microsoft.Identity.Web and Microsoft.Identity.Web.UI NuGet packages.
// Note after implememting the newer library for authetication, we had to modify the _LoginPartial.cshtml file.
//services.AddAuthentication(AzureADDefaults.AuthenticationScheme)
// .AddAzureAD(options => Configuration.Bind("AzureAd", options));
///////////////////////////////////
// Add services required for using options.
// e.g used for calling Graph Api from WebOptions class, from config file.
services.AddOptions();
// Add service for MS Graph API Service Client.
services.AddTransient<OidcConnectEvents>();
// Sign-in users with the Microsoft identity platform
services.AddSignIn(Configuration);
// Token acquisition service based on MSAL.NET
// and chosen token cache implementation
services.AddWebAppCallsProtectedWebApi(Configuration, new string[] { Constants.ScopeUserRead })
.AddInMemoryTokenCaches();
// Add the MS Graph SDK Client as a service for Dependancy Injection.
services.AddGraphService(Configuration);
///////////////////////////////////
// The following lines code instruct the asp.net core middleware to use the data in the "roles" claim in the Authorize attribute and User.IsInrole()
// See https://learn.microsoft.com/aspnet/core/security/authorization/roles?view=aspnetcore-2.2 for more info.
services.Configure<OpenIdConnectOptions>(OpenIdConnectDefaults.AuthenticationScheme, options =>
{
// The claim in the Jwt token where App roles are available.
options.TokenValidationParameters.RoleClaimType = "roles";
});
// Adding authorization policies that enforce authorization using Azure AD roles. Polices defined in seperate classes.
services.AddAuthorization(options =>
{
options.AddPolicy(AuthorizationPolicies.AssignmentToViewLogsRoleRequired, policy => policy.RequireRole(AppRole.ViewLogs));
});
///////////////////////////////////
services.AddRazorPages().AddMvcOptions(options =>
{
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
options.Filters.Add(new AuthorizeFilter(policy));
}).AddMicrosoftIdentityUI();
// Adds the service for creating the Jwt Token used for calling microservices.
// Note we are using our independant bearer token issuer service here, NOT Azure AD
services.AddScoped<JwtService>();
}
Sample Razor PageModel method:
public void OnGet()
{
var username = HttpContext.User.Identity.Name;
var forename = HttpContext.User.Claims.FirstOrDefault(c => c.Type == "given_name")?.Value;
var surname = HttpContext.User.Claims.FirstOrDefault(c => c.Type == "family_name")?.Value;
_logger.LogInformation("" + username + " requested the Index page");
}
UPDATE
Getting closer to a solution but not quite there yet. Couple of issues resolved:
I originally created the Tenant in Azure to use B2C AD, even though I was no longer using B2C and had switched to Azure AD. It wasn't until I deleted the tenant and created a new one before I started to see the optional claims come through to the webapp correctly. After creating the new tenant and assigning the tenant type to use Azure AD, I then found that the 'Token Configuration' menu was now available for configuring the optional claims through the UI, it seems that modifying the App manifest is still required as well, as shown above.
I had to add the 'profile' scope as type 'delegated' to the webapp API Permissions in Azure.
The final issue still unresolved is that although I can see the claims present during Debug, I cant figure out how to retrieve the claim values.
In the method below, I can see the required claims when using Debug, but can't figure out how to retrieve the values:
public void OnGet()
{
var username = HttpContext.User.Identity.Name;
var forename = HttpContext.User.Claims.FirstOrDefault(c => c.Type == "given_name")?.Value;
var surname = HttpContext.User.Claims.FirstOrDefault(c => c.Type == "family_name")?.Value;
_logger.LogInformation("" + username + " requested the Index page");
}
Debug Screenshots shows the given_name & family_name are present:
I've tried different code examples using the claims principal to try and get the values out, but nothing is working for me. Hoping this final riddle is fairly simple to someone who knows the required syntax, as said we now have the required optional claims present, its just not knowing how to actually get the values out.
Big thanks to 'Dhivya G - MSFT Identity' for their assistance (see comments below my original question) method below now allows me to access the required claim values from the Token ID returned from Azure following successful login.
public void OnGet()
{
var username = HttpContext.User.Identity.Name;
var forename = HttpContext.User.Claims.FirstOrDefault(c => c.Type == ClaimTypes.GivenName)?.Value;
var surname = HttpContext.User.Claims.FirstOrDefault(c => c.Type == ClaimTypes.Surname)?.Value;
_logger.LogInformation("" + username + " requested the Index page");
}

Identity Server 4 GetSchemeSupportsSignOutAsync returns incorrect response

I've setup an open id connect provider, Google in this case, using the AddOpenIdConnect extension method in dotnet core. From the discovery document:
https://accounts.google.com/.well-known/openid-configuration
it does not seem that google supports federated sign-out because there is no end_session endpoint. However, in Identity Server 4, the call:
var providerSupportsSignout = await HttpContext.GetSchemeSupportsSignOutAsync(idp);
returns true. So during Logout it tries to sign out of google using:
return SignOut(new AuthenticationProperties { RedirectUri = url }, vm.ExternalAuthenticationScheme);
which throws an exception:
InvalidOperationException: Cannot redirect to the end session endpoint, the configuration may be missing or invalid.
Is this a bug in Identity Server 4 or is there a configuration property that needs to be set when setting up the Oidc provider so that this extension method will pickup that the provider does not support signout?
Doesn't appear to be a bug in Identity Server 4. The code behind this extension calls out to get the underlying authentication scheme handler.
public static async Task<bool> GetSchemeSupportsSignOutAsync(this HttpContext context, string scheme)
{
var provider = context.RequestServices.GetRequiredService<IAuthenticationHandlerProvider>();
var handler = await provider.GetHandlerAsync(context, scheme);
return (handler != null && handler is IAuthenticationSignOutHandler);
}
In this case, your handler will be OpenIdConnectHandler which appears to implement IAuthenticationSignOutHandler so that's why regardless of what is in the discovery document (end session endpoint supported or not), if you use the AddOpenIdConnect(...), it will always register a handler which seemingly supports sign out, but as you have pointed out, does not actually enforce the actual idp validation for that kind of functionality support (link to handler source).
And lastly, worthwhile to mention, that Identity Server 4 check is rightful here as according to Microsoft docs, the IAuthenticationSignOutHandler is indeed basically a marker interface used to determine if a handler supports SignOut.
So I guess you just simply can't use the generic AddOpenIdConnect(...), instead perhaps you should use AddGoogle(...) which does not implement IAuthenticationSignOutHandler so will work as expected with Identity Server 4 (link to source).
As Vidmantas Blazevicius mentioned, using .AddOpenIdConnect will make the extension method default to true because of the interface. I have changed my code to explicity check for the support of an end_session_endpoint by doing:
var discoveryClient = new IdentityModel.Client.DiscoveryClient("https://accounts.google.com/.well-known/openid-configuration")
{
Policy = new IdentityModel.Client.DiscoveryPolicy
{
ValidateEndpoints = false, //this is needed for google, if set to true then will result in error response
ValidateIssuerName = false //this is needed for Microsoft, if set to true then will result in error response
}
};
var discoveryResult = await discoveryClient.GetAsync();
if (!discoveryResult.IsError)
{
if (!String.IsNullOrWhiteSpace(discoveryResult.EndSessionEndpoint))
supportsFederatedSignOut = true;
}
I then save an additional property on the model "SupportsFederatedSignOut" and then use this to determine whether external identity provider signout (SignOut) should be called.

Accessing authenticated Google Cloud Endpoints API from Google Apps Script

I'm trying to pull some data into a Google sheets spreadsheet from an API that's been built using Google Cloud Endpoints. Here is the API declaration:
#Api(
name = "myendpoint",
namespace =
#ApiNamespace
(
ownerDomain = "mydomain.com",
ownerName = "mydomain.com",
packagePath = "myapp.model"
),
scopes = {SCOPES},
clientIds = {ANDROID_CLIENT_ID, WEB_CLIENT_ID, API_EXPLORER_CLIENT_ID},
audiences = {WEB CLIENT_ID}
)
The method I'm trying to access has authentication enabled by means of the user parameter in the API declaration:
#ApiMethod(name = "ping", httpMethod = HttpMethod.GET, path = "ping")
public StringResponse getPing(User user) throws OAuthRequestException {
CheckPermissions(user);//throws an exception if the user is null or doesn't have the correct permissions
return new StringResponse("pong");
}
This works fine when using the generated client libraries or the gapi js library. However AFAIK I can't use those js libraries in Apps Script.
I've got an OAuth2 flow working using the apps-script-oauth2 library from here, and I'm pretty much using the default setup for creating the service
function getService() {
// Create a new service with the given name. The name will be used when
// persisting the authorized token, so ensure it is unique within the
// scope of the property store.
return OAuth2.createService(SERVICE_NAME)
// Set the endpoint URLs, which are the same for all Google services.
.setAuthorizationBaseUrl('https://accounts.google.com/o/oauth2/auth')
.setTokenUrl('https://accounts.google.com/o/oauth2/token')
// Set the client ID and secret, from the Google Developers Console.
.setClientId(CLIENT_ID)
.setClientSecret(CLIENT_SECRET)
// Set the name of the callback function in the script referenced
// above that should be invoked to complete the OAuth flow.
.setCallbackFunction('ruggedAuthCallback')
// Set the property store where authorized tokens should be persisted.
.setPropertyStore(PropertiesService.getUserProperties())
// Set the scopes to request (space-separated for Google services).
.setScope(SCOPES)
// Below are Google-specific OAuth2 parameters.
// Sets the login hint, which will prevent the account chooser screen
// from being shown to users logged in with multiple accounts.
.setParam('login_hint', Session.getActiveUser().getEmail())
// Requests offline access.
.setParam('access_type', 'offline')
// Forces the approval prompt every time. This is useful for testing,
// but not desirable in a production application.
.setParam('approval_prompt', 'auto')
//.setParam('include_granted_scopes', 'true');
}
These are my methods for accessing the APIs
function getDriveDocs() {
return executeApiMethod('https://www.googleapis.com/drive/v2/','files?maxResults=10');
}
function pingServer(){
return executeApiMethod('https://myapp.appspot.com/_ah/api/myendpoint/v1/','ping');
}
function executeApiMethod(apiUrl, method)
{
//var url = ;
var url = apiUrl + method;
var service = getRuggedService();
return UrlFetchApp.fetch(url, {
'muteHttpExceptions': true,
'method': 'get',
'headers': {
Authorization: 'Bearer ' + service.getAccessToken()
}
});
}
The getDriveDocs() method works perfectly, so I know my auth flow is working correctly. Also, if I call an unauthenticated method in my API I get the correct response. However, when I call the authenticated 'ping' method, the 'user' parameter is always null. Am I missing something in the fetch call? Everything I've read so far seems to suggest that setting
Authorization: 'Bearer ' + service.getAccessToken()
should be enough.
Any help would be much appreciated!
This turned out to be a simple mistake - I had created a new oauth2 credential in the google dev console and had not added the new client id to the API declaration. Here is the working API declaration:
#Api(
name = "myendpoint",
namespace =
#ApiNamespace
(
ownerDomain = "mydomain.com",
ownerName = "mydomain.com",
packagePath = "myapp.model"
),
scopes = {SCOPES},
clientIds = {ANDROID_CLIENT_ID, WEB_CLIENT_ID, API_EXPLORER_CLIENT_ID, GAPPS_CLIENT_ID},
audiences = {WEB CLIENT_ID}
)

Resources