I was testing around with Google's oauth and was trying out different scopes.
However, I then reduced my scope request to just this : "https://www.googleapis.com/auth/userinfo.email"
The following is more in dotnetcore
Dictionary<string, string> queries = new Dictionary<string, string>();
queries.Add("scope", "https://www.googleapis.com/auth/userinfo.email");
queries.Add("access_type", "offline");
queries.Add("include_granted_scopes" ,"true");
queries.Add("response_type", "code");
queries.Add("state", "state");
queries.Add("redirect_uri", "http://localhost:5000/api/authenticate/googauth");
queries.Add("client_id", _clientId);
queries.Add("prompt", "consent");
UriBuilder builder = new UriBuilder();
builder.Host = "accounts.google.com";
builder.Scheme = "https";
builder.Path = "o/oauth2/v2/auth";
//builder.Query = ""
foreach (var query in queries)
{
if (string.IsNullOrEmpty(builder.Query))
{
builder.Query += $"{query.Key}={query.Value}";
}
else
{
builder.Query += $"&{query.Key}={query.Value}";
}
}
var redirectUri = builder.Uri.ToString();
return Redirect(redirectUri);
From the returned code, I then retrieved the access token etc.
Dictionary<string, string> values = new Dictionary<string, string>();
values.Add("code", code);
values.Add("client_id", _clientId);
values.Add("client_secret",_clientSecret);
values.Add("redirect_uri", "http://localhost:5000/api/authenticate/googauth");
values.Add("grant_type", "authorization_code");
var client = new HttpClient();
var result = await client.PostAsync("https://oauth2.googleapis.com/token", new FormUrlEncodedContent(values));
var content = await result.Content.ReadAsStringAsync();
var convertedContent = JsonSerializer.Deserialize<GoogleAccesstoken>(content);
However, I seem to get more than what I asked for. I get this in the returned scopes :
openid https://www.googleapis.com/auth/user.gender.read https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/user.birthday.read
I've tried using incognito, and different browsers and they all return the same thing (thinking that it may have been a cache issue).
Is anyone able to help me on this?
Thanks.
Enables applications to use incremental authorization to request access to additional scopes in context. If you set this parameter's value to true and the authorization request is granted, then the new access token will also cover any scopes to which the user previously granted the application access. See the incremental authorization section for examples.
extract from google documentation: https://developers.google.com/identity/protocols/oauth2/web-server
Basically means that the user has previously granted you the other scopes. Could have been through a login screen or something where you have used the same clientId
Related
i'm currently trying to connect via UNO-Plattform sample to the Spotify API.
https://github.com/unoplatform/Uno.Samples/blob/master/UI/Authentication.OidcDemo/Authentication.OidcDemo/Authentication.OidcDemo.Shared/MainPage.xaml.cs
Therefore I have updated the PrepareClient method.
private async void PrepareClient()
{
var redirectUri = WebAuthenticationBroker.GetCurrentApplicationCallbackUri().OriginalString;
// Create options for endpoint discovery
var options = new OidcClientOptions
{
Authority = "https://accounts.spotify.com", //"https://demo.duendesoftware.com/",
ClientId = "7c1....a45",
ClientSecret = "4b..a",
Scope = "playlist-read-private",
RedirectUri = redirectUri,
PostLogoutRedirectUri = redirectUri,
ResponseMode = OidcClientOptions.AuthorizeResponseMode.Redirect,
Flow = OidcClientOptions.AuthenticationFlow.AuthorizationCode
};
// Create the client. In production application, this is often created and stored
// directly in the Application class.
_oidcClient = new OidcClient(options);
var extra_parameters = new Dictionary<string, string>();
//extra_parameters.Add("response_type", "token"); // if i add this line i get an error
_loginState = await _oidcClient.PrepareLoginAsync(extra_parameters);
btnSignin.IsEnabled = true;
// Same for logout url.
//If i add this line a get an error
//_logoutUrl = new Uri(await _oidcClient.PrepareLogoutAsync(new LogoutRequest()));
btnSignout.IsEnabled = true;
}
private async void SignIn_Clicked(object sender, RoutedEventArgs e)
{
var startUri = new Uri(_loginState.StartUrl);
// Important: there should be NO await before calling .AuthenticateAsync() - at least
// on WebAssembly, in order to prevent triggering the popup blocker mechanisms.
var userResult = await WebAuthenticationBroker.AuthenticateAsync(WebAuthenticationOptions.None, startUri);
if (userResult.ResponseStatus != WebAuthenticationStatus.Success)
{
txtAuthResult.Text = "Canceled";
// Error or user cancellation
return;
}
// User authentication process completed successfully.
// Now we need to get authorization tokens from the response
var authenticationResult = await _oidcClient.ProcessResponseAsync(userResult.ResponseData, _loginState);
if (authenticationResult.IsError)
{
var errorMessage = authenticationResult.Error;
// TODO: do something with error message
txtAuthResult.Text = $"Error {errorMessage}";
return;
}
// That's completed. Here you have to token, ready to do something
var token = authenticationResult.AccessToken;
var refreshToken = authenticationResult.RefreshToken;
// TODO: make something useful with the tokens
txtAuthResult.Text = $"Success, token is {token}";
}
If i use Postman for authentication, i can use the URL
curl --location --request GET 'https://accounts.spotify.com/authorize?response_type=token&client_id=7c...45&scope=playlist-read-private&redirect_uri=http://localhost:8080&state=test'
and everything works fine and i get the token in the callback url as parameter.
If i add as "extra_parameters" the "response_type" : "token" i get the message, that this parameter is not supported...
I'm a little bit stucked here and don't know how to proceed.
I'm happy about any help in every direction to get this autentication done with uno-plattform.
OIDC can be described as a superset of OAuth2. It is a way for an identity provider to issue tokens and supply info about a user via additional APIs. Read more here.
The Oidc code that you use (probably IdentityModel.OidcClient?) requires a the service you’re calling to implement a few extra endpoints which Spotify has not implemented for their API. This is discussed in this forum topic. Because of the missing Oidc support, your code will try making calls that do not work.
The SpotifyAPI-NET library might also help you authenticate and make API calls instead.
I am developing an ASP.NET MVC app with Azure B2C authentication. It is required that, after the ID token expires (IIS session not expires), any subsequent action call should automatically refresh the ID token with the refresh token and then continue the execution without re-login.
Questions:
Does the solution make sense?
After refreshing the ID token and set the cookies, how can I redirect to the original url and continue execution without re-login?
Thanks, any idea is highly appreciated.
This is my code:
protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
{
var refreshToken = HttpContext.Current.Request.Cookies["msal.refreshtoken"];
if (refreshToken != null && !string.IsNullOrEmpty(refreshToken.Value))
{
var newIdToken = TokenService.RefreshIdToken(refreshToken.Value);
var idTokenCookie = new HttpCookie("msal.idtoken", newIdToken)
{
Secure = true,
HttpOnly = true
};
HttpContext.Current.Response.Cookies.Set(idTokenCookie);
return;
}
}
// TokenService.RefreshIdToken
public static string RefreshIdToken(string refreshToken)
{
var policyName = ConfigurationManager.AppSettings["ida:SignUpSignInPolicyId"];
var B2CDomain = ConfigurationManager.AppSettings["ida:B2CDomain"];
var tenant = ConfigurationManager.AppSettings["ida:Tenant"];
var clientId = ConfigurationManager.AppSettings["ida:ClientId"];
var clientSecret = ConfigurationManager.AppSettings["ida:ClientSecret"];
var tokenEndpointUri = $"https://{B2CDomain}/{tenant}/{policyName}/oauth2/v2.0/token";
var httpClient = new HttpClient();
var requestBodyDict = new Dictionary<string, string>
{
{ "grant_type" , "refresh_token" },
{ "client_id" , clientId },
{ "client_secret" , clientSecret },
{ "scope" , $"openid" },
{ "refresh_token" , refreshToken }
};
var request = new HttpRequestMessage
{
RequestUri = new Uri(tokenEndpointUri),
Method = HttpMethod.Post,
Content = new FormUrlEncodedContent(requestBodyDict)
};
var task = Task.Run(() => httpClient.SendAsync(request));
task.Wait();
var response = task.Result;
var task1 = Task.Run(() => response.Content.ReadAsStringAsync());
task1.Wait();
var responseString = task1.Result;
if (response.IsSuccessStatusCode)
{
var idToken = (string)JsonConvert.DeserializeObject<dynamic>(responseString).id_token.ToString();
return idToken;
}
else
{
throw new Exception();
}
}
A couple of thoughts that are too long to put in comments:
Yes the basic idea of ‘use the refresh token to get a new id token’ is how it’s supposed to work.
Googling this question suggests a bewildering array of examples to imitate :-( e.g. Microsoft’s Azure Samples on GitHub for A/D auth for a web app (as opposed to webapi or SPA)
The basic plan for identity problems like this is, find an authoritative example and follow it because that reduces your risk of embarrassing error. ( For instance, Auth0’s example for this scenario says to get a new refresh_token as well as a new id_token. Not doing that might be okay but then the user will be forced to re-login when the refresh token expires. Then you’ll be tempted to use ultra-long-lifetime refresh token, loosening your security a little)
If you can’t find an authoritative example, considering raising an issue or commenting on one.
OTOH, if the code you’ve written works, then maybe you’ve done!
The problem with finding an example to imitate after you’ve got started is trying to find just the right the example for the technology choices you already made. It may be easier to start with an empty project, follow a tutorial, get the tutorial working, then copy the solution back into your app.
To send your user back to their original target you should be able to
var originalUrl= HttpContext.Current.Request.Url;
HttpContext.Current.Response.Redirect(original);
But only do that if getting the id_token succeeded otherwise it creates an infinite loop.
I have been testing some code to sign in users to their Microsoft/school/work accounts using raw HttpRequestMessage and HttpResponseMessage. I know there are libraries available to do this but I want to test the raw approach as well (especially usage of refresh tokens), while looking for the right library to handle it.
I'm currently learning authentication, with limited knowledge of ASP.NET/Core.
I'm following this guide: https://learn.microsoft.com/en-us/graph/auth-v2-user
I've just modified the SignIn() method in AccountController in an example project that used more high level libraries to sign in.
I'm requesting an authorization code.
The SignIn() code:
public void SignIn()
{
using (var httpClient = new HttpClient())
{
try
{
var tenant = "my tenant id";
var clientId = ConfigurationManager.AppSettings["ida:AppID"];
var responseType = "id_token+code";
var redirectURI = ConfigurationManager.AppSettings["ida:RedirectUri"];
var responseMode = "form_post";//query";
var appScopes = ConfigurationManager.AppSettings["ida:AppScopes"];
var scopes = $"openid profile offline_access {appScopes}";
var state = "12345";
//var prompt = "consent";
var url = string.Format("https://login.microsoftonline.com/{0}/oauth2/v2.0/authorize", tenant);
var body = string.Format("client_id={1}&response_type={2}&redirect_uri={3}&response_mode={4}&scope={5}&state={6}", tenant, clientId, responseType, redirectURI, responseMode, scopes, state);
var request = new HttpRequestMessage(HttpMethod.Post, url);
request.Content = new StringContent(body, Encoding.UTF8, "application/x-www-form-urlencoded");
var response = httpClient.SendAsync(request, HttpCompletionOption.ResponseContentRead).Result;
var content = response.Content.ReadAsStringAsync().Result;
}
catch (Exception ex)
{
}
}
//if (!Request.IsAuthenticated)
//{
// // Signal OWIN to send an authorization request to Azure
// Request.GetOwinContext().Authentication.Challenge(
// new AuthenticationProperties { RedirectUri = "/" },
// OpenIdConnectAuthenticationDefaults.AuthenticationType);
//}
}
I'm just returning void from the method now because I'm not sure what I should return yet.
Debugging and looking at the response variable, the status code is 200, and has some other information to it. However, the content of the HttpResponseMessage, when I paste it into a file and opening it in a browser, displays (or redirects to) https://login.microsoftonline.com/cookiesdisabled, which shows a message saying that I could not be logged in because my browser blocks cookies. However, I don't think this really is the case.
How can I resolve this and have the user log in and consent, and get the authorization code?
I couldn't really find any example in ASP.NET that uses this raw approach. Is it not recommended?
You should fistly understand how OAuth 2.0 authorization code flow works in Azure AD V2.0 :
Microsoft identity platform and OAuth 2.0 authorization code flow
The general process would be like :
When login in client application, user will be redirect to Azure AD login endpoint(https://login.microsoftonline.com/{0}/oauth2/v2.0/authorize) and provides info like which client(client_id) in which tenant(tenant id) user wants to login , and redirect back to which url(redirect_uri) after successful login.
User enter credential , Azure AD validate credential and issue code and redirect user back to redirect url provided in step 1 (Also match one of the redirect_uris you registered in the portal).
The client application will get the code and send http post request with code to acquire access token .
So if you want to manally implement the code flow in your application , you can refer to below code sample :
public async Task<IActionResult> Login()
{
string authorizationUrl = string.Format(
"https://login.microsoftonline.com/{0}/oauth2/v2.0/authorize?response_type=code&client_id={1}&redirect_uri={2}&scope={3}",
"tenantID", "ClientID", "https://localhost:44360/Home/CatchCode",
"openid offline_access https://graph.microsoft.com/user.read");
return Redirect(authorizationUrl);
}
private static readonly HttpClient client = new HttpClient();
public async Task<ActionResult> CatchCode(string code)
{
var values = new Dictionary<string, string>
{
{ "grant_type", "authorization_code" },
{ "client_id", "XXXXXX"},
{ "code", code},
{ "redirect_uri", "https://localhost:44360/Home/CatchCode"},
{ "scope", "https://graph.microsoft.com/user.read"},
{ "client_secret", "XXXXXXXXXXX"},
};
var content = new FormUrlEncodedContent(values);
//POST the object to the specified URI
var response = await client.PostAsync("https://login.microsoftonline.com/cb1c3f2e-a2dd-4fde-bf8f-f75ab18b21ac/oauth2/v2.0/token", content);
//Read back the answer from server
var responseString = await response.Content.ReadAsStringAsync();
//you can deserialize an Object use Json.NET to get tokens
}
That just is simple code sample which will get Microsoft Graph's access token , you still need to care about url encode and catch exception , but it shows how code flow works .
I have created a custom Authorize attribute where I use the Office Graph to get AAD groups the current user is member of, and based on those I reject or authorize the user. I want to save the groups, because the call to Office Graph takes some performance. What would be the correct way to save that kind of data? I can see some people saves it to a SQL server, but then I would need to ensure cleanup etc.
Also I can see in some threads the session state is stated to be a bad choice due to concurrency. So the question is what options do you have to store this kind of information?
All suggestions are welcome.
If you were only using the group_id info, there is no need to use Office Graph and store it at all. We can enable Azure AD issue the groups claims by change the manifest of Azure AD like below:(refer this code sample)
"groupMembershipClaims": "All",
And if you are also using other info about groups, you can store these info into claims. Here is a code sample that add the name of groups into claims for your reference:
AuthorizationCodeReceived = async context =>
{
ClientCredential credential = new ClientCredential(ConfigHelper.ClientId, ConfigHelper.AppKey);
string userObjectId = context.AuthenticationTicket.Identity.FindFirst(Globals.ObjectIdClaimType).Value;
AuthenticationContext authContext = new AuthenticationContext(ConfigHelper.Authority, new TokenDbCache(userObjectId));
AuthenticationResult result = await authContext.AcquireTokenByAuthorizationCodeAsync(
context.Code, new Uri(HttpContext.Current.Request.Url.GetLeftPart(UriPartial.Path)), credential, ConfigHelper.GraphResourceId);
ActiveDirectoryClient graphClient = new ActiveDirectoryClient(new Uri(ConfigHelper.GraphServiceRoot),
async () => { return await Task.FromResult(result.AccessToken); }
);
try
{
foreach (var groupClaim in context.AuthenticationTicket.Identity.FindAll("groups"))
{
var request = new HttpRequestMessage()
{
RequestUri = new Uri($"https://graph.windows.net/adfei.onmicrosoft.com/groups/{groupClaim.Value}?api-version=1.6"),
Method = HttpMethod.Get,
};
request.Headers.Authorization = new AuthenticationHeaderValue("bearer", result.AccessToken);
using (HttpClient httpClient = new HttpClient())
{
HttpResponseMessage httpResponse = httpClient.SendAsync(request).Result;
var retJSON = httpResponse.Content.ReadAsStringAsync().Result;
var dict = new JavaScriptSerializer().Deserialize<Dictionary<string, object>>(retJSON);
((ClaimsIdentity)context.AuthenticationTicket.Identity).AddClaim(new Claim("groupName", dict["displayName"].ToString()));
}
}
}
catch (Exception ex)
{
}
},
Then we can these info from controller using the code below:
ClaimsPrincipal.Current.FindAll("groupName")
Based on this tutorial http://www.asp.net/aspnet/overview/owin-and-katana/owin-oauth-20-authorization-server, I have created an Authorization Server, a Resource Server and a MVC Client.
The MVC Client has a Controller which gets some data from the Resource Server. The Resource Server requires authentication. The MVC Clients gets an authorization code from the Authorization Server and Redirects the user to the Authorization Server for authentication. Finally the MVC Clients exchanges the authorization code for a Access token to Access the Resource Server. This is the Authorization code flow as described by the OAuth 2 protocol. This works fine.
Now, I have the requirement to make a Controller of the MVC Client itself require Authentication. I can not find a tutorial for this.
I added
app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions());
to my Startup.Auth.cs.
I assume, I need to setup the Options to Redirect to the Authorization Server. I can also set the Provider on the Options:
app.UseOAuthBearerAuthentication(new Microsoft.Owin.Security.OAuth.OAuthBearerAuthenticationOptions()
{
Provider = new OAuthBearerAuthenticationProvider()
});
But I am also stuck on implementing the events of the Provider.
Can anybody guide me in the right direction? Or are there any tutorials which might help me?
I ended up with a solution based on these two articles from Brock Allen:
http://brockallen.com/2013/10/24/a-primer-on-owin-cookie-authentication-middleware-for-the-asp-net-developer/
http://brockallen.com/2014/01/09/a-primer-on-external-login-providers-social-logins-with-owinkatana-authentication-middleware/
The fundemental idea is to register two authentication Middlewares. An active Cookie-Authentication and a passive OAuthBearer-Authentication. In Startup.Auth.cs they are added like this:
app.UseCookieAuthentication(new CookieAuthenticationOptions()
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/ExternalLogin/Login"),
});
app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions()
{
AuthenticationType = DefaultAuthenticationTypes.ExternalBearer,
AuthenticationMode = Microsoft.Owin.Security.AuthenticationMode.Passive,
});
You also add an ExternalLogin-Controller. Its Login-method has to redirect the user to the Login-page of your Authorization Server to get the authorization code. You have to supply a callback function where you will process the authorization code.
public async Task<ActionResult> Login(string returnUrl)
{
if (string.IsNullOrEmpty(returnUrl) && Request.UrlReferrer != null)
returnUrl = Server.UrlEncode(Request.UrlReferrer.PathAndQuery);
if (Url.IsLocalUrl(returnUrl) && !string.IsNullOrEmpty(returnUrl))
_returnUrl = returnUrl;
//callback function
_redirectUrl = Url.Action("AuthorizationCodeCallback", "ExternalLogin", null, Request.Url.Scheme);
Dictionary<string, string> authorizeArgs = null;
authorizeArgs = new Dictionary<string, string>
{
{"client_id", "0123456789"}
,{"response_type", "code"}
,{"scope", "read"}
,{"redirect_uri", _redirectUrl}
// optional: state
};
var content = new FormUrlEncodedContent(authorizeArgs);
var contentAsString = await content.ReadAsStringAsync();
return Redirect("http://localhost:64426/oauth/authorize?" + contentAsString);
}
In your callback-function you exchange the authorization code for an access token (plus refresh token) challenge your passive OAuthBearer-authentication Middleware and signin with the Access token as your Cookie.
public async Task<ActionResult> AuthorizationCodeCallback()
{
// received authorization code from authorization server
string[] codes = Request.Params.GetValues("code");
var authorizationCode = "";
if (codes.Length > 0)
authorizationCode = codes[0];
// exchange authorization code at authorization server for an access and refresh token
Dictionary<string, string> post = null;
post = new Dictionary<string, string>
{
{"client_id", "0123456789"}
,{"client_secret", "ClientSecret"}
,{"grant_type", "authorization_code"}
,{"code", authorizationCode}
,{"redirect_uri", _redirectUrl}
};
var client = new HttpClient();
var postContent = new FormUrlEncodedContent(post);
var response = await client.PostAsync("http://localhost:64426/token", postContent);
var content = await response.Content.ReadAsStringAsync();
// received tokens from authorization server
var json = JObject.Parse(content);
_accessToken = json["access_token"].ToString();
_authorizationScheme = json["token_type"].ToString();
_expiresIn = json["expires_in"].ToString();
if (json["refresh_token"] != null)
_refreshToken = json["refresh_token"].ToString();
//SignIn with Token, SignOut and create new identity for SignIn
Request.Headers.Add("Authorization", _authorizationScheme + " " + _accessToken);
var ctx = Request.GetOwinContext();
var authenticateResult = await ctx.Authentication.AuthenticateAsync(DefaultAuthenticationTypes.ExternalBearer);
ctx.Authentication.SignOut(DefaultAuthenticationTypes.ExternalBearer);
var applicationCookieIdentity = new ClaimsIdentity(authenticateResult.Identity.Claims, DefaultAuthenticationTypes.ApplicationCookie);
ctx.Authentication.SignIn(applicationCookieIdentity);
var ctxUser = ctx.Authentication.User;
var user = Request.RequestContext.HttpContext.User;
//redirect back to the view which required authentication
string decodedUrl = "";
if (!string.IsNullOrEmpty(_returnUrl))
decodedUrl = Server.UrlDecode(_returnUrl);
if (Url.IsLocalUrl(decodedUrl))
return Redirect(decodedUrl);
else
return RedirectToAction("Index", "Home");
}
I hope this is useful for someone who is implementing the OAuth authorization code flow in his MVC 5 application.
I used official sample MVC Implicit Client which I believe is the correct authentication flow for MVC application.
For authorization I used this getting started, especially the part about infinite loop when roles are specified [Authorize(Roles = "Foo,Bar")] and user is authenticated but doesn't own any of these.