Getting Gmail API access and ID token, but refresh token is NULL - oauth-2.0

Following https://developers.google.com/identity/sign-in/web/server-side-flow After getting the authorization code from JavaScript, and passing it to the server side, we indeed get an access token (and an ID token), but not the required refresh token.
There are many posts around but could not solve it yet.
Any suggestion how to get the refresh token?
thanks!
private String getResponseToken(GoogleClientSecrets clientSecrets,
String authCode) throws IOException {
try {
GoogleTokenResponse tokenResponse =
new GoogleAuthorizationCodeTokenRequest(
new NetHttpTransport(),
JacksonFactory.getDefaultInstance(),
"https://www.googleapis.com/oauth2/v4/token",
// "https://accounts.google.com/o/oauth2/token",
clientSecrets.getDetails().getClientId(),
clientSecrets.getDetails().getClientSecret(),
authCode, //NOTE: was received from JavaScript client
"postmessage" //TODO: what's this?
).execute();
String accessToken = tokenResponse.getAccessToken();
String idToken = tokenResponse.getIdToken();
//TODO: not getting a refresh token... why?!
String refreshToken = tokenResponse.getRefreshToken();
Boolean hasRefreshToken = new Boolean(!(refreshToken == null));
LOGGER.warn("received refresh token: {}", hasRefreshToken);
LOGGER.debug("accessToken: {}, refreshToken: {}, idToken: {}", accessToken, refreshToken, idToken);
return accessToken;
}catch (TokenResponseException tre){...}

Gmail API only gives the refresh token the first time you ask for the users permission. (At least this is what happens to me).
Go to: https://myaccount.google.com/permissions?pli=1, remove the authorization to your app and run your code. You should receive the refresh token.

you should add the
AccessType = "offline"
You need to call the function
new GoogleAuthorizationCodeRequestUrl(...).setAccessType("offline")
or another syntax:
var authReq = new GoogleAuthorizationCodeRequestUrl(new Uri(GoogleAuthConsts.AuthorizationUrl)) {
RedirectUri = Callback,
ClientId = ClientId,
AccessType = "offline",
Scope = string.Join(" ", new[] { Scopes... }),
ApprovalPrompt = "force"
};
in Fiddler you should see the following request:
https://accounts.google.com/o/oauth2/auth?scope=https://www.googleapis.com/auth/webmasters&redirect_uri=http://mywebsite.com/google/scapi/callback/&response_type=code&client_id=xxx&access_type=offline
see also here
More details about setAccessType can be found here

after finding how to use the Google APIs at the backend (documentation is somewhat partial..), the issue was fixed at the FrontEnd side by tweaking a parameter:
grantOfflineAccess({
- prompt: 'select_account'
+ prompt: 'consent'
HTH

Related

Difference between oauth2/v2 vs oidc Spotify API

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.

.Net Core Graph API Token Issue - "Access token has expired or is not yet valid"

I am developing a .Net 6 application, hosted in an Azure App Service and using Azure AD Authentication.
When viewing a Request page, I would like to check if the user belongs to an Azure Ad Group. This works sometimes, but users will periodically get an error when trying to view the page: "Access token has expired or is not yet valid."
I assume the token is being expired as if the user clears their cookies, AAD will re-authenticate them creating a new token and all is fine again, but but I haven't been able to find anything around refreshing tokens and am not sure where to go from here.
Has anyone experienced this behaviour and found a solution for it?
Here are some relevant sections of the code
Startup.cs File:
string[] initialScopes = Configuration.GetValue<string>("GraphAPI:Scopes")?.Split(' ');
services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(Configuration.GetSection("AzureAd"))
.EnableTokenAcquisitionToCallDownstreamApi(options =>
{
Configuration.Bind("AzureAd", options);
}, initialScopes)
.AddInMemoryTokenCaches()
.AddMicrosoftGraph(options =>
{
options.Scopes = String.Join(' ', initialScopes);
});
AADGroupFunctions.cs
AADGroupFunctions.cs
private readonly GraphServiceClient _graphServiceClient;
public AADGroupFunctions(GraphServiceClient graphServiceClient)
{
_graphServiceClient = graphServiceClient;
}
public async Task<List<IADLookupModel>> FindUsersInGroup(string groupId)
{
var listOfUsers = new List<IADLookupModel>();
var filterString = $"startswith(mail, '{groupId}')";
var groups = await _graphServiceClient.Groups
.Request()
.Header("ConsistencyLevel", "eventual")
.Filter(filterString)
.Expand("members")
.Top(1)
.GetAsync();
if (groups.Any())
{
if (groups.First().Members.Any())
{
foreach (Microsoft.Graph.User user in groups.First().Members)
{
try
{
var mail = "";
if (user.Mail != null)
{
mail = user.Mail.ToLower();
listOfUsers.Add(new UserModel()
{
DisplayName = user.DisplayName,
UPN = user.UserPrincipalName.ToLower(),
Email = mail,
Description = user.JobTitle ?? ""
});
}
}
catch (Exception)
{
}
}
}
}
return listOfUsers;
}
Error Message when trying to call the FindUsersInGroup() function:
An unhandled exception occurred while processing the request.
ServiceException: Code: InvalidAuthenticationToken Message: Access token has expired or is not yet valid. Inner error: AdditionalData: date: 2022-02-21T17:37:46 request-id: [removed] client-request-id: [removed] ClientRequestld: [removed] Microsoft.Graph.HttpProvider.SendAsync(HttpRequestMessage request, HttpCompletionOption completionOption, CancellationToken cancellationToken)
Routing
The access token have a short lifetime, sometimes like an hour or even shorter. So you need to use a refresh token to ask AzureAd for a new access token when the current one is about to expire.
see this link https://learn.microsoft.com/en-us/azure/active-directory/develop/refresh-tokens

Sign In using raw HttpRequestMessage in ASP.NET MVC

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 .

How to handle posts with IdentityServer3 as authentication server

TL;DR
How do you POST data in an ASP.NET MVC project (form, jQuery, axios), using IdentityServer3 as the authentication server. Also, what flow to use, to make this work?
What I'm experiencing
I have a working IdentityServer3 instance. I also have an ASP.NET MVC project. Using hybrid flow, as I will have to pass the user's token to other services. The authentication itself works - when the pages are only using GET. Even if the authenticated user's tokens are expired, something in the background redirects the requests to the auth. server, and the user can continue it's work, without asking the user to log in again. (As far as I understand, the hybrid flow can use refresh tokens, so I assume that's how it can re-authenticate the user. Even if HttpContext.Current.User.Identity.IsAuthenticated=false)
For testing purposes, I set the AccessTokenLifetime, AuthorizationCodeLifetime and IdentityTokenLifetime values to 5 seconds in the auth. server. As far as I know, the refresh token's expire time measured in days, and I did not change the default value.
But when I try to use POST, things get "ugly".
Using form POST, with expired tokens, the request gets redirected to IdentityServer3. It does it's magic (the user gets authenticated) and redirects to my page - as a GET request... I see the response_mode=form_post in the URL, yet the posted payload is gone.
Using axios POST, the request gets redirected to IdentityServer3, but fails with at the pre-flight OPTIONS request.
Using the default jQuery POST, got same error. (Even though, the default jQuery POST uses application/x-www-form-urlencoded to solve the pre-flight issue.)
startup.cs
const string authType = "Cookies";
// resetting Microsoft's default mapper
JwtSecurityTokenHandler.InboundClaimTypeMap = new Dictionary<string, string>();
// ensure, that the MVC anti forgery key engine will use our "custom" user id
AntiForgeryConfig.UniqueClaimTypeIdentifier = "sub";
app.UseCookieAuthentication(new Microsoft.Owin.Security.Cookies.CookieAuthenticationOptions
{
AuthenticationType = authType
});
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
ClientId = clientId,
RedirectUri = adminUri,
PostLogoutRedirectUri = adminUri,
Authority = idServerIdentityEndpoint,
SignInAsAuthenticationType = authType,
ResponseType = "code id_token",
Scope = "openid profile roles email offline_access",
Notifications = new OpenIdConnectAuthenticationNotifications
{
#region Handle automatic redirect (on logout)
RedirectToIdentityProvider = async n =>
{
// if signing out, add the id_token_hint
if (n.ProtocolMessage.RequestType ==
OpenIdConnectRequestType.LogoutRequest)
{
var token = n.OwinContext.Authentication.User.FindFirst(idTokenName);
if (token != null)
{
var idTokenHint =
token.Value;
n.ProtocolMessage.IdTokenHint = idTokenHint;
}
}
},
#endregion
AuthorizationCodeReceived = async n =>
{
System.Diagnostics.Debug.Print("AuthorizationCodeReceived " + n.ProtocolMessage.ToString());
// fetch the identity from authentication response
var identity = n.AuthenticationTicket.Identity;
// exchange the "code" token for access_token, id_token, refresh_token, using the client secret
var requestResponse = await OidcClient.CallTokenEndpointAsync(
new Uri(idServerTokenEndpoint),
new Uri(adminUri),
n.Code,
clientId,
clientSecret
);
// fetch tokens from the exchange response
identity.AddClaims(new []
{
new Claim("access_token", requestResponse.AccessToken),
new Claim("id_token", requestResponse.IdentityToken),
new Claim("refresh_token", requestResponse.RefreshToken)
});
// store the refresh_token in the session, as the user might be logged out, when the authorization attribute is executed
// see OrganicaAuthorize.cs
HttpContext.Current.Session["refresh_token"] = requestResponse.RefreshToken;
// get the userinfo from the openId endpoint
// this actually retreives all the claims, but using the normal access token
var userInfo = await EndpointAndTokenHelper.CallUserInfoEndpoint(idServerUserInfoEndpoint, requestResponse.AccessToken); // todo: userinfo
if (userInfo == null) throw new Exception("Could not retreive user information from identity server.");
#region Extract individual claims
// extract claims we are interested in
var nameClaim = new Claim(Thinktecture.IdentityModel.Client.JwtClaimTypes.Name,
userInfo.Value<string>(Thinktecture.IdentityModel.Client.JwtClaimTypes.Name)); // full name
var givenNameClaim = new Claim(Thinktecture.IdentityModel.Client.JwtClaimTypes.GivenName,
userInfo.Value<string>(Thinktecture.IdentityModel.Client.JwtClaimTypes.GivenName)); // given name
var familyNameClaim = new Claim(Thinktecture.IdentityModel.Client.JwtClaimTypes.FamilyName,
userInfo.Value<string>(Thinktecture.IdentityModel.Client.JwtClaimTypes.FamilyName)); // family name
var emailClaim = new Claim(Thinktecture.IdentityModel.Client.JwtClaimTypes.Email,
userInfo.Value<string>(Thinktecture.IdentityModel.Client.JwtClaimTypes.Email)); // email
var subClaim = new Claim(Thinktecture.IdentityModel.Client.JwtClaimTypes.Subject,
userInfo.Value<string>(Thinktecture.IdentityModel.Client.JwtClaimTypes.Subject)); // userid
#endregion
#region Extract roles
List<string> roles;
try
{
roles = userInfo.Value<JArray>(Thinktecture.IdentityModel.Client.JwtClaimTypes.Role).Select(r => r.ToString()).ToList();
}
catch (InvalidCastException) // if there is only 1 item
{
roles = new List<string> { userInfo.Value<string>(Thinktecture.IdentityModel.Client.JwtClaimTypes.Role) };
}
#endregion
// attach the claims we just extracted
identity.AddClaims(new[] { nameClaim, givenNameClaim, familyNameClaim, subClaim, emailClaim });
// attach roles
identity.AddClaims(roles.Select(r => new Claim(Thinktecture.IdentityModel.Client.JwtClaimTypes.Role, r.ToString())));
// update the return value of the SecurityTokenValidated method (this method...)
n.AuthenticationTicket = new AuthenticationTicket(
identity,
n.AuthenticationTicket.Properties);
},
AuthenticationFailed = async n =>
{
System.Diagnostics.Debug.Print("AuthenticationFailed " + n.Exception.ToString());
},
MessageReceived = async n =>
{
System.Diagnostics.Debug.Print("MessageReceived " + n.State.ToString());
},
SecurityTokenReceived = async n =>
{
System.Diagnostics.Debug.Print("SecurityTokenReceived " + n.State.ToString());
},
SecurityTokenValidated = async n =>
{
System.Diagnostics.Debug.Print("SecurityTokenValidated " + n.State.ToString());
}
}
});
Have you configured cookie authentication middleware in the MVC app? After the authentication with identity server, an authentication cookie should be set. When the authentication cookie is set and valid IdentityServer redirection will not occur until the cookie expires/deleted.
Update 1:
Ok, I misunderstood the quesion. It is logical to redirect to identity server when session times out. It won't work with post payload. You can try doing something like follows.
If the request is a normal post, redirect user again to the form
fill page.
If request is ajax post, return unauthorized result and based on
that response refresh the page from javascript.
Anyway I don't think you will be able to keep the posted data unless you are designing your own solution for that. (e.g keep data stored locally).
But you might be able to avoid this scenario altogether if you carefuly decide identity server's session timeout and your app's session timeout.
In OpenIdConnectAuthenticationOptions set UseTokenLifetime = false that will break connection between identity token's lifetime and cookie session lifetime.
In CookieAuthenticationOptions make sliding expiration
SlidingExpiration = true,
ExpireTimeSpan = TimeSpan.FromMinutes(50),
Now you are incontrol of your apps session lifetime. Adjust it to match your needs and security conserns.

Outlook Office 365 : Refresh token failed to retrieve because "AADSTS70000" the provided value for the 'code' parameter is not valid

In below code I can retrieve refresh token successfully from email#company.com email addresses. However, when I try to login with email#outlook.com it doesn't give the refresh token instead it returns this response.
Response:
{
"error": "invalid_grant",
"error_description": "AADSTS70000: The provided value for the 'code' parameter is not valid. The code has expired.\r\nTrace ID: ...\r\nCorrelation ID: ...\r\nTimestamp: 2016-05-19 10:13:05Z",
"error_codes": [
70000
],
"timestamp": "2016-05-19 10:13:05Z",
"trace_id": "8cceb393-....",
"correlation_id": "5227de8...."
}
Code:
private async Task<string> GetRefreshRoken(string authCode, string onSuccessRedirectUri) {
var client = new HttpClient();
var parameters = new Dictionary<string, string>
{
{"client_id", _clientId},
{"client_secret", _clientSecret},
{"code",authCode }, // what retreived from //https://login.microsoftonline.com/common with authroization.
{"redirect_uri", onSuccessRedirectUri}, //http://localhost:27592/Home/Authorize
{"grant_type","authorization_code" }
};
var content = new FormUrlEncodedContent(parameters);
var response = await client.PostAsync("https://login.microsoftonline.com/common/oauth2/v2.0/token", content);
var tokensJsonString = await response.Content.ReadAsStringAsync();
dynamic token = Newtonsoft.Json.JsonConvert.DeserializeObject(tokensJsonString);
return token.refresh_token;
}
So I had googled with the error number and found http://www.matvelloso.com/2015/01/30/troubleshooting-common-azure-active-directory-errors/ page where the error describes:
Then I had changed my redirecting url to "http://localhost:27592/Home/Authorize/". Since I am using this https://dev.outlook.com/restapi/tutorial/dotnet tutorial as a reference , now I cannot login with any other account.
Is there any good approach to retrieve refresh tokens for outlook account?
For windows live id account, you will get error "The provided value for the 'code' parameter is not valid. The code has expired." when using the authorization code twice.
The correct way to refresh the token is using refresh token (v2.0 token reference > Refresh Token).
First, ensure you have declare the scope "offline_access".
Then, you will get the access_token when acquire the token using grant_type=code (the first time you acquire the token).
Next, you need to use grant_type=refresh_token to refresh your access token.

Resources