I'm wondering if anyone has solved managing the expiration of Google OAuth2 tokens?
The example below is a REST call to get a list of calendars with a valid token. If the token is expired, I will get a 401 response and need to refresh the token using the 'refresh token' stored in my database. I'm wondering if anyone has a strategy around this for their web application?
var httpContent = new HttpRequestMessage(HttpMethod.Get, "https://www.googleapis.com/calendar/v3/users/me/calendarList");
httpContent.Headers.Add("Authorization", "OAuth " + token);
responseBody = client.SendAsync(httpContent).Result.EnsureSuccessStatusCode().Content.ReadAsStringAsync().Result;
I've thought about refreshing automatically upon user logging in and saving the refresh token encrypted in session, but not sure if there are any better strategies.
//Notes
Strategy Options:
1 - Upon Logging a user in, refresh all OAuth tokens for user. This works assuming that a session will never last longer than a token timeout period. (may not be reliable for all OAuth servers).
2 - When refreshing, use token expiration to record expiration date/time in database. Before calling any API, check if token needs to be refreshed. (still need to account for edge case where token expires unexpectedly outside of normal expiration schedule)
3 - Catch the response status of the call and check for 401s. If receive a 401, refresh the token and try again. This could be a fail-over for both Options 1/2. Code for this example would be here:
var restClient = new RestClient();
var request = new RestRequest("https://www.googleapis.com/calendar/v3/users/me/calendarList", Method.GET);
request.AddHeader("Authorization", "OAuth " + token);
// execute the request
var response = restClient.Execute(request);
if (response.StatusCode == HttpStatusCode.Unauthorized)
{
var newToken = RefreshGoogleToken(token);
request = new RestRequest("https://www.googleapis.com/calendar/v3/users/me/calendarList", Method.GET);
request.AddHeader("Authorization", "OAuth " + newToken);
// execute the request
response = restClient.Execute(request);
}
var content = response.Content; // raw content as string
dynamic responseJson = JsonValue.Parse(content);
var calendarList = new List<GoogleCalendar>();
foreach (var item in responseJson.items)
{
var calendar = new GoogleCalendar { Kind = item.kind, Etag = item.etag, Id = item.id, Title = item.summary, Description = item.description, Location = item.location, Timezone = item.timeZone, SummaryOverride = item.summaryOverride, ColorId = item.colorId, AccessRole = item.accessRole };
if (item.defaultReminders != null)
{
calendar.DefaultReminders = new List<GoogleCalendarReminder>();
foreach (var reminder in item.defaultReminders)
{
var rem = new GoogleCalendarReminder { Method = reminder.method, Minutes = reminder.minutes };
calendar.DefaultReminders.Add(rem);
}
}
calendarList.Add(calendar);
}
return calendarList;
}
If the expiration time is known you can keep track of your token's expiration time and anticipate that it has already expired, then just do the refresh at that time.
I would imagine you could also simply respond to the 401 errors with a refresh as well, and even as a fail safe if the expiration tracking happens to fail (just be sure to add code to avoid getting stuck in a loop).
Related
I have the following code:
public async Task<TokenResponse> RefreshTokenAsync(string refreshToken)
{
HttpClient client = new();
var discoveryResponse = await client.GetDiscoveryDocumentAsync("https://localhost:44334");
var response = await client.RequestRefreshTokenAsync(new RefreshTokenRequest
{
Address = discoveryResponse.TokenEndpoint,
ClientId = "...",
ClientSecret = "...",
RefreshToken = refreshToken
});
return response;
}
And it always returns 400 Bad Request with invalid_client message. When I'm refreshing token in Postman it works well. Where is the problem?
The purpose of the refresh-token is: the user does not need to re-authenticate with the credentials (username/password) in the application every time the session expires. So your application needs to connect to the endpoint identity and consume a new refresh token before the token or refresh token times out. In asp dotnet core Identity and JwtToken always have a default timeout value; whatever: you need to capture the refresh token before this timeout expires, otherwise your identity understands the user who does not have the browser open or is not online. This may imply developing a routine that stays in Roudin-Robin always refreshing the application with the new Token while the browser is open.
I changed my code to this:
public async Task<TokenResponse> RefreshTokenAsync(string refreshToken)
{
HttpClient client = new();
var discoveryResponse = await client.GetDiscoveryDocumentAsync("https://localhost:44334");
var tokenClient = new TokenClient(client, new TokenClientOptions
{
Address = discoveryResponse.TokenEndpoint,
ClientId = "...",
ClientSecret = "...",
});
var response = await tokenClient.RequestRefreshTokenAsync(refreshToken);
response.HttpResponse.EnsureSuccessStatusCode();
return response;
}
And now it works as expected.
I'm struggling with a problem of how to update the users Access/Id token on expiry I know how to get new tokens, i'm trying to work out how to update the users current/expired token once I have the updated ones? following code works fine and the response returns new access/id tokens, a null session (which is odd). I have tried GetUserReequest using the currrent access token then updating it, to no effect. Documentation is a bit sparse on methods that expose access token also
public async Task RefreshTokens(string token)
{
var request = new AdminInitiateAuthRequest
{
UserPoolId = userpool,
ClientId = clientId,
AuthFlow = AuthFlowType.REFRESH_TOKEN_AUTH,
AuthParameters =
{
{"REFRESH_TOKEN", token},
{"SECRET_HASH", secret}
}
};
var response = await _cognitoIdentityProvider.AdminInitiateAuthAsync(request);
//Do something here to update users tokens
}
I'm working on a multilanguage project for accademic purpose. I've written a simple Python Client that make requests to an API server written in ASP.NET. The server retrives spotify info about users. The server interacts with a DB filled by a Golang server that only makes scraping on API's exposed from Spotify. I'm aware that it's a misuse and there are better solutions
Clearly, Golang server, in order to make requests to Spotify API's, needs to know the access token returned from spotify Authorization Code Flow. Overlooking about spotify token expire time, the idea is: after user authentication through Identity module of ASP.NET server (using JWT token), associate the access token obtained calling https://accounts.spotify.com/api/token to user's informations. So, i expose an API in ASP.NET server like this
[AllowAnonymous]
[HttpPost("token")]
public async Task<ContentResult> getTokenAsync(string? code = null)
{
//to retrive information about who is the user that making call -> need later for associate spotifytoken
string accessToken = Request.Headers[HeaderNames.Authorization].ToString().Replace("Bearer ", "");
JwtSecurityTokenHandler t = new JwtSecurityTokenHandler();
var token = t.ReadJwtToken(accessToken);
var user = _userManager.FindByIdAsync(token.Subject).Result;
string s = "https://accounts.spotify.com/api/token";
if (code == null)
{
var qb = new QueryBuilder();
qb.Add("response_type", "code");
qb.Add("client_id", _config["SpotiSetting:clientId"]);
qb.Add("scope", "user-read-private user-read-email user-library-read");
qb.Add("redirect_uri", _config["SpotiSetting:redirectUser"]);
qb.Add("show_dialog", "true");
return new ContentResult
{
ContentType = "text/html",
Content = "https://accounts.spotify.com/authorize/" + qb.ToQueryString().ToString()
//Content = JsonConvert.SerializeObject(user.Result)
};
} else
{
//if i'm here, api is the callback designed for spotify
var qb = new QueryBuilder();
qb.Add("grant_type", "authorization_code");
qb.Add("code", code);
qb.Add("redirect_uri", "https://localhost:44345/spotify/token");
var client = new HttpClient();
var req = new HttpRequestMessage(HttpMethod.Post, s);
req.Content = new FormUrlEncodedContent(qb);
req.Headers.Authorization = new AuthenticationHeaderValue("Basic", "here_my_secret_encoded_CLIENTID:CLIENT_SECRET");
var response = await client.SendAsync(req);
var result = response.Content.ReadAsStringAsync().Result;
AccessToken json = JsonConvert.DeserializeObject<AccessToken>(result);
user.spotifyInformation.authToken = code;
user.spotifyInformation.accessToken = json;
var res = _userManager.UpdateAsync(user);
if (res.IsCompletedSuccessfully)
{
return Content("ok");
}
else
{
Content("Problem");
}
} return Content("");
}
The problem is that the second time that API is invoked, it's spotify that is sending the first authorization token (needed to request access_token), so I lost user information retrived in the first request. Should be better write two distinct API and separate callback from user request?
It's my first question here, so please to have mercy
Scenario: I want to get user access token of the fb page admin by JS login and retrieving token ONE TIME, and will store that in database. Then daily, I want to do wall post to those page.
I am using JS to get the initial token and storing it. Then using c# FacebookSDK for the web requests.
FB.login(function (response) {
var r = response;
// get access token of the user and update in database
$("#FacebookAccessToken").val(response.authResponse.accessToken);
},
{
scope: 'manage_pages,publish_stream'
});
Now I am saving this token in database as I will be using this for future graph calls - is this right?
On server side when I need to do a post after a day I retrieve that token and do the processing as below:
// step 1 get application access token
var fb1 = new FacebookClient();
dynamic appTokenCLient = fb1.Get("oauth/access_token", new
{
client_id = appId,
client_secret = appSecret,
grant_type = "client_credentials",
scope = "manage_pages,publish_stream",
redirect_uri = siteUrl
});
var fbTokenSettingVal = GetTokenFromDB(); // getting access token from database which was stored during JS fb login
// step 2 extend token
var fb2 = new FacebookClient(appTokenCLient.access_token);
dynamic extendedToken = fb2.Get("oauth/access_token", new
{
client_id = appId,
client_secret = appSecret,
grant_type = "fb_exchange_token",
fb_exchange_token = fbTokenSettingVal.Val
});
var userExtendedToken = extendedToken.access_token; // get extended token and update database
// step 3 get page access token from the pages, that the user manages
var fb3 = new FacebookClient { AppId = appId, AppSecret = appSecret, AccessToken = userExtendedToken };
var fbParams = new Dictionary<string, object>();
var publishedResponse = fb3.Get("/me/accounts", fbParams) as JsonObject;
var data = JArray.Parse(publishedResponse["data"].ToString());
var pageToken = "";
foreach (var account in data)
{
if (account["name"].ToString().ToLower().Equals("PAGE_NAME"))
{
pageToken = account["access_token"].ToString();
break;
}
}
// step 4 post a link to the page - throws error !
var fb4 = new FacebookClient(pageToken);
fb4.Post("/PAGE_NAME/feed",
new
{
link = "http://www.stackoverflow.com"
});
The last 4th step throws error, when posting to selected page:
The user hasn't authorized the application to perform this action
Have tried several different ways, but in vain. Assume that there is just a simple step which I am doing wrong here.
Also, is it ok to extend the fb access token for user every time before making request?
Any way to check if token is expired or not?
If you want to use that access token for future. You need to take offline_access token and that token you need to store in database.
This offline accesstoken will be expire once user will change the password or delete your application from facebook account.
private void GetPermenentAccessTokenOfUser(string accessToken)
{
var client2 = new FacebookClient(accessToken);
//get permenent access token
dynamic result = client2.Post("oauth/access_token", new
{
client_id = _apiKey,
client_secret = _apiSecret,
grant_type = "fb_exchange_token",
fb_exchange_token = accessToken
});
_accessToken = result.access_token;
}
Looks like for new apps we need to apply for manage_pages permission from our application:
which I am doing now. As it shows error when doing login:
Also, the app needs to be live, so they can reproduce this permission and verify that we do need this permission to post to pages. Its good for fb users safety but a time taking process for developers.
Any way this can be skipped for testing purpose?
I use dotnetOpenAuth. I want to request authorization to the user's gamil.
Do I need to use openId first?
Cannot find a decent tutorail. Can anyone help?
Tried this code unsuccesfully. Anyway I don't seems to ask for Gmail scope at the auth request, so I'm confused
public void PrepareAuthorizationRequest(Uri authCallbakUrl)
{
var consumer = new WebConsumer(GoogleConsumerConsts.ServiceDescription, mConsumerTokenManager);
// request access
consumer.Channel.Send(consumer.PrepareRequestUserAuthorization(authCallbakUrl, null, null));
throw new NoRedirectToAuthPageException();
}
public ProcessAuthorizationRequestResponse ProcessAuthorizationRequest()
{
ProcessAuthorizationRequestResponse response;
// Process result from the service provider
var consumer = new WebConsumer(GoogleConsumerConsts.ServiceDescription, mConsumerTokenManager);
var accessTokenResponse = consumer.ProcessUserAuthorization();
// If we didn't have an access token response, this wasn't called by the service provider
if (accessTokenResponse == null)
response = new ProcessAuthorizationRequestResponse
{
IsAuthorized = false
};
else
{
// Extract the access token
string accessToken = accessTokenResponse.AccessToken;
response = new ProcessAuthorizationRequestResponse
{
IsAuthorized = true,
Token = accessToken,
Secret = mConsumerTokenManager.GetTokenSecret(accessToken)
};
}
return response;
}
private string Test2()
{
// Process result from linked in
var google = new WebConsumer(GoogleConsumerConsts.ServiceDescription, mConsumerTokenManager);
// var accessToken = GetAccessTokenForUser();
var accessToken = String.Empty;
// Retrieve the user's profile information
var endpoint = GoogleConsumerConsts.GetGmailFeedsEndpoint;// new MessageReceivingEndpoint("http://api.linkedin.com/v1/people/~", HttpDeliveryMethods.GetRequest);
var request = google.PrepareAuthorizedRequest(endpoint, accessToken);
var response = request.GetResponse();
return (new StreamReader(response.GetResponseStream())).ReadToEnd();
}
No, you don't need to use OpenID if you just want to access the user's Gmail. OpenID is for when you want to authenticate the user. OAuth is for when you want to access the user's data.
You need to include the scope parameter in your authorization request as described in this question: Adding scopes to OAuth 1.0 authorization request with DotNetOpenAuth.