I'm implementing Azure B2C to a .NET MVC app, and I need to add an extra query parameter to the login url.
Here's how I've set it up in the startup.cs
var openIdConnectAuthenticationOptions = new OpenIdConnectAuthenticationOptions
{
// Generate the metadata address using the tenant and policy information
MetadataAddress = String.Format(Globals.WellKnownMetadata, Globals.Tenant, Globals.DefaultPolicy),
// These are standard OpenID Connect parameters, with values pulled from web.config
ClientId = Globals.ClientId,
RedirectUri = Globals.RedirectUri,
PostLogoutRedirectUri = Globals.RedirectUri,
// Specify the callbacks for each type of notifications
Notifications = new OpenIdConnectAuthenticationNotifications
{
RedirectToIdentityProvider = OnRedirectToIdentityProvider,
AuthorizationCodeReceived = OnAuthorizationCodeReceived,
AuthenticationFailed = OnAuthenticationFailed
},
// Specify the claim type that specifies the Name property.
TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = "name",
ValidateIssuer = false
},
// Specify the scope by appending all of the scopes requested into one string (separated by a blank space)
Scope = $"openid",
ResponseType = "id_token",
};
app.UseOpenIdConnectAuthentication(
openIdConnectAuthenticationOptions
);
And when someone tries to visit an [authorized] tagged page, it sends them to this b2c url:
https://mytenant.b2clogin.com/mytenant.onmicrosoft.com/oauth2/v2.0/authorize?p=b2c_custom_signin&client_id=0000-000000-000-00&redirect_uri=https://localhost&response_type=id_token&scope=openid&x-client-SKU=ID_NET461&x-client-ver=5.3.0.0
However, I need to add an extra query parameter onto the end, "&appId=000-000-000", so the resulting login URL is:
https://mytenant.b2clogin.com/mytenant.onmicrosoft.com/oauth2/v2.0/authorize?p=b2c_custom_signin&client_id=0000-000000-000-00&redirect_uri=https://localhost&response_type=id_token&scope=openid&x-client-SKU=ID_NET461&x-client-ver=5.3.0.0 &appId=000-000-000
how would I go about doing this?
I'm afraid you could not add the appId parameter, but I recommend to make use of the state parameter. You could use this parameter to send the value of appid as part of request and it gets returned back in response.
For more details, see here.
Related
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"]);
}
}
i 'm working on an azure functions that make some graph call to different tenant (multitenant)
I want to reuse a GraphServiceClient and leveraging token cache
I generate the GraphServiceClient in this way:
List<string> scopes = new List<string>() { "https://graph.microsoft.com/.default" };
var authProvider = ConfidentialClientApplicationBuilder.Create("e9b93362-a788-4644-8623-da9f4d4776a7")
.WithAuthority(AzureCloudInstance.AzurePublic, AadAuthorityAudience.AzureAdMultipleOrgs)
.WithClientSecret("fkpx53225awyQJDHV35:^][")
.Build();
var dd = new MsalAuthenticationProvider(authProvider, scopes.ToArray(),"ugochotmail.onmicrosoft.com");
var appGraphClient = new GraphServiceClient(dd);
Than i should call
authResult = await _clientApplication.AcquireTokenForClient(_scopes)
.WithAuthority(AzureCloudInstance.AzurePublic, Tenant)
.ExecuteAsync();
To obtain a token for the app to access the specific tenant.
The problem is in the authentication provider that is call on every send request but doen't offer a parameter with the tenant name
public async Task AuthenticateRequestAsync(HttpRequestMessage request)
{
var token = await GetTokenAsync();
request.Headers.Authorization = new AuthenticationHeaderValue("bearer", token);
}
At the moment i just add a property to the Authentication provider to set the tenant. It works but i would like to know if there is a better solution
Per my understanding, it seems your function doesn't allow a parameter which specify the tenant name and then use the tenant name when do GetTokenAsync() method. And now you can just hardcode the tenant name in the line new MsalAuthenticationProvider(... to specify the tenant.
For this problem, I think you can add a variable named tenant in the "Application settings" of your function app (as below screenshot show).
Then add a line of code string tenant = System.Environment.GetEnvironmentVariable("tenant"); above var token = await GetTokenAsync();
After that, you can add parameter in method GetTokenAsync() like GetTokenAsync(tenant). Then you do not need to hardcode tenant name in code, you just need to change the tenant name in "Application settings" of your function.
If I misunderstand your requirement, please provide more details.
=============================Update===============================
It seems you just want to specify the tenant in your code by a parameter, but not add the tenant name as a property in var dd = new MsalAuthenticationProvider(authProvider, scopes.ToArray(),"tenant name");. If so, you can refer to the code below (just add a line .WithTenantId("xxx.onmicrosoft.com") when do ConfidentialClientApplicationBuilder)
No it doesn't fix the problem as, in a multitenant, the target tenant is send as a parameter to the function. I'm working on an other approach i will come back when i will finish tests.
Thanks a lot
Using Identity Server 4 server.
In client’s Startup.Auth.cs :
private static void ConfigureAuth(IAppBuilder app)
{
ISettingsReader settingsReader = Services.Resolve<ISettingsReader>();
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
ExpireTimeSpan = new TimeSpan(1, 0, 0),
CookieSecure = CookieSecureOption.Always,
CookieHttpOnly = true,
SlidingExpiration = true
});
var platformUri = settingsReader.GetSetting("PlatformUri")?.TrimEnd('/');
var platformApiKey = settingsReader.GetSetting("PlatformApiKey");
var deploymentURL = settingsReader.GetSetting("deploymentURL")?.TrimEnd('/');
var authority = $"{platformUri}/identity";
string clientSecret;
string clientId = SplitApiKey(platformApiKey, out clientSecret);
var options = new OpenIdConnectAuthenticationOptions
{
ClientId = clientId,
ClientSecret = clientSecret,
Authority = authority,
RedirectUri = $"{deploymentURL}/signin/callback",
ResponseType = "id_token token",
Scope = "platform openid",
UseTokenLifetime = false,
SignInAsAuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
Notifications = new OpenIdConnectAuthenticationNotifications
{
SecurityTokenValidated = SecurityTokenValidatedHandler,
RedirectToIdentityProvider = RedirectToIdentityProviderHandler,
}
};
app.UseOpenIdConnectAuthentication(options);
AntiForgeryConfig.UniqueClaimTypeIdentifier = ClaimTypes.NameIdentifier;
}
Note – the cookie’s expire timespan has been set to 5 mins for debugging, normally would be set to an hour.
And then stash the access_token in the validated handler (as per several articles) so that we can use it later for api calls:
private static async Task SecurityTokenValidatedHandler(SecurityTokenValidatedNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions> notification)
{
var jwtDetails = JsonWebToken.Parse(notification.ProtocolMessage.AccessToken);
notification.AuthenticationTicket.Identity.AddClaims(
jwtDetails.Claims.Where(c => DesiredAccessTokenClaims.Contains(c.Type)));
notification.AuthenticationTicket.Identity.AddClaim(new Claim("access_token", notification.ProtocolMessage.AccessToken));
notification.AuthenticationTicket.Identity.AddClaim(new Claim("id_token", notification.ProtocolMessage.IdToken));
}
This works fine. However, although the cookies do auto refresh themselves, with sliding expiration (typically after 2-3 minutes, rather than the full 5), there doesn’t seem to be a way to refresh the access token which is held in the claim, so although the user will remain logged in, the access token will be useless after it expires.
Is this the right way to be going about this? And if so, is there a way to update the access token in the claim, in the background without disturbing the user? It seems that the ideal solution would be to have the cookie refresh also trigger the SecurityTokenValidatedHandler so that a renewed claim can be added into the new cookie, although despite looking in CookieManager, etc. there doesn't seem to be an event which is triggered when the cookie sliding refreshes itself. Does anyone know a way to do this?
Many thanks for your time!
If anyone comes across this issue, the answer was to change from implicit to hybrid. Hybrid allows refresh tokens, whereas implicit, not so much. In our case we needed Hybrid and Client Creds
I've run the on-behalf-of sample in .NET 4.5:
https://github.com/Azure-Samples/active-directory-dotnet-webapi-onbehalfof
But I need to do the same thing in .NET core. When trying to port the sample service to .NET core everything compiles except for this line:
var bootstrapContext = ClaimsPrincipal.Current.Identities.First().BootstrapContext as System.IdentityModel.Tokens.BootstrapContext;
It won't compile because System.IdentityModel isn't available in .NET Core.
I discovered that if I acquire the user access token and the user name using this code instead it works for users in the same tenant as the service:
System.Security.Claims.ClaimsIdentity identity =
User.Identity as System.Security.Claims.ClaimsIdentity;
string userAccessToken = identity.BootstrapContext as string;
string userName = (User.FindFirst(ClaimTypes.Upn))?.Value;
But for users in a different tenant (multi-tenant authentication), userAccessToken and userName are set to null. What do I need to do differently to get this working for users coming from a different tenant?
I figured out that the reason userAccessToken and userName were coming back as null is because I was missing the SaveSigninToken parameter when calling UseJwtBearerAuthentication in Startup.cs:
app.UseJwtBearerAuthentication(new JwtBearerOptions
{
AutomaticAuthenticate = true,
AutomaticChallenge = true,
Authority = String.Format(CultureInfo.InvariantCulture, Configuration["Authentication:AzureAd:AADInstance"], Configuration["Authentication:AzureAd:Tenant"]),
Audience = Configuration["Authentication:AzureAd:Audience"],
TokenValidationParameters = new TokenValidationParameters { SaveSigninToken = true, ValidateIssuer = false }
});
Without SaveSigninToken = true, the caller's access token and identity doesn't flow through in the case where the caller is from another tenant.
Instead of using the BootstrapContext class to get the access_token, we can use the HttpContext in the controller directly to get the orignal access_token via code below:
var orignalToken = HttpContext.Request.Headers["authorization"][0].Split(' ')[1];
I'm currently writing an angular application that first authenticates against think texture identityserver3.
This works fine, and I receive the bearer token without any issues.
When I use my token on an call to my API, I'm authenticated. I can see my userid, but have lost my claims (username, roles,...).
What do I have to do for transferring my claims with my token, or getting the roles from the identityserver?
You can tell Identity Server to include specific claims in an access token by adding that claim to your API's Scope.
Example:
var apiScope = new Scope {
Name = "myApi",
DisplayName = "My API",
Type = ScopeType.Resource,
Claims = new List<ScopeClaim> {
new ScopeClaim("myClaimType")
}
};
You can also use the AlwaysIncludeInIdToken property of ScopeClaim to include the claims in identity tokens as well as access tokens.
See https://identityserver.github.io/Documentation/docsv2/configuration/scopesAndClaims.html for more info.
We are doing something very similar using MS Web API 2 and a Thinktecture Identity Server v3.
To verify the user's claims we created an Authentication Filter, and then called the Identity server directly to get the user's claims. The bearer token only grants authentication and it is up to the API to get the claims separately.
protected override bool IsAuthorized(HttpActionContext actionContext)
{
string identityServerUrl = WebConfigurationManager.AppSettings.Get("IdentityServerUrl") + "/connect/userinfo";
using (var httpClient = new HttpClient())
{
httpClient.DefaultRequestHeaders.Authorization = actionContext.Request.Headers.Authorization;
var response = httpClient.GetAsync(identityServerUrl).Result;
if (response.IsSuccessStatusCode)
{
string responseString = response.Content.ReadAsStringAsync().Result;
Dictionary<string, string> claims = JsonConvert.DeserializeObject<Dictionary<string, string>>(responseString.ToLower());
... Do stuff with your claims here ...
}
}
}