RESTful API using JWT (JSON Web Token) setup token expiry - api

I'm using JWT for RESTful API (Laravel Web-Services for mobile). How to setup token expiry to never expiry or what the best practice to setup token expiry?
Because currently i need to get the token everytime when the token expired, can anybody have this issue or best solution for token expiry.

There is nothing to make the token never expire. However you can extend the expiration date to a very huge time span, 1 year for example. This is possible, however it is not recommended for security.
In order to achieve that, you need to configure two parts, the token refresh time, and token expiry.
So in config/jwt.php
'refresh_ttl' => 29030400, // Number of minutes in 1 year (12*4*7*24*60*60)
And when you are creating your token, you can pass something like the following
$tokenId = base64_encode(mcrypt_create_iv(32, MCRYPT_DEV_URANDOM));
$issuedAt = Carbon::now()->timestamp;
$notBefore = $issuedAt; //Adding 10 seconds
$expire = $notBefore + 12*4*7*24*60*60; // Adding 6 hours
/*
* Create the token as an array
*/
$data = [
'iat' => $issuedAt, // Issued at: time when the token was generated
'jti' => $tokenId, // Json Token Id: an unique identifier for the token
'iss' => 'https://example.com', // Issuer
'nbf' => $notBefore, // Not before
'exp' => $expire, // Expire
'data' => [ // Data related to the signed user
'userId' => Auth::user()->id, // userid from the users table
]
];
Now, your token will never expire before 1 year. And you have up to 1 year to refresh it. When the user opens the application the next time, and you authenticate the token, you can refresh it. You can refresh the token, as mentioned in the documentation here. I would recommend going through this laracasts discussion as well.
Also, I have found this question on StackOverflow, I think it will help.

Related

AWS Cognito - Programatically get refresh token expiry

Refresh token returned from Cognito is not a JWT token , hence cannot be decoded. Is there a way to get the refresh token expiry or it needs to be maintained at application level.
There is no way to decode a refresh token. If you know the expiration time set in cognito for refresh tokens you can store the time it was generated and calculate based on that.
just to elaborate on the accepted answer, as I had the same question.
jwt.org cannot decode the refresh token from aws, as it is encrypted
My way around it, is as follows:
the id token carries "auth_time" (see https://docs.aws.amazon.com/cognito/latest/developerguide/amazon-cognito-user-pools-using-the-id-token.html)
on login (you could technically do it on refresh as well), I look at that value and add expiration duration to that for a rough estimate
how to get the expiration duration programmatically? There are probably easier ways to do it, but the sdk-v3 command that worked for me was the 'DescribeUserPoolClientCommand' (https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/clients/client-cognito-identity-provider/classes/describeuserpoolclientcommand.html)
pseudo code in typescript (used in nodejs backend code) looks something like this:
import { CognitoIdentityProviderClient, DescribeUserPoolClientCommand, DescribeUserPoolClientCommandInput} from "#aws-sdk/client-cognito-identity-provider"
import get from 'lodash/get'
const client = new CognitoIdentityProviderClient({ region: [yourRegion] })
const input = {
UserPoolId: [yourUserPoolId],
ClientId: [yourWebClientId],
} as DescribeUserPoolClientCommandInput
const command = new DescribeUserPoolClientCommand(input)
const response = await client.send(command)
const refreshTokenValidityUnits = get(
response,
"UserPoolClient.TokenValidityUnits.RefreshToken"
)
const refreshTokenValidity = get(
response,
"UserPoolClient.RefreshTokenValidity"
)
// result: "days" and "30" for example
This is obviously not complete enough to get the exact values, but enough to get anyone started who, like me, might not be as familiar with the aws-sdk yet.

OpenIddict Refresh Token Flow issue ASP.NET Core

I am trying to set my refresh token life time to 2 weeks. I have tried via .. FromSeconds, FromMinutes, FromHours, but it always sets the refresh token to the same lifetime as the access token. I would appreciate any help. This is what I currently have in my configureServices:
services.AddOpenIddict(options =>
{
// Register the Entity Framework stores.
options.AddEntityFrameworkCoreStores<AppDbContext>();
// Register the ASP.NET Core MVC binder used by OpenIddict.
// Note: if you don't call this method, you won't be able to
// bind OpenIdConnectRequest or OpenIdConnectResponse parameters.
options.AddMvcBinders();
// Enable the token endpoint.
options.EnableTokenEndpoint("/connect/token");
// Enable the password flow.
options.AllowPasswordFlow()
.AllowRefreshTokenFlow()
.SetAccessTokenLifetime(TimeSpan.FromMinutes(1))
.SetRefreshTokenLifetime(TimeSpan.FromMinutes(20160));
// During development, you can disable the HTTPS requirement.
options.DisableHttpsRequirement();
});
Please note: the following post won't resolve the issue:
http://kosmisch.net/Blog/DotNetEssential/Archive/2017/9/11/openiddict-refresh-token-flow-issue-aspnet-core-20.html
Latest summary about the issue:
When login with user name and password, the following data is inserted into openiddicttoken table:
Id ApplicationId AuthorizationId Ciphertext End Hash Start Status Subject Type
1 NULL NULL NULL 2017-10-12 11:24:26.0000000 +00:00 NULL 2017-09-12 11:24:26.0000000 +00:00 valid 1 refresh_token
Then a refresh_token grant type request was done.
The above record is updated with only change is the Status column, which has changed from valid to redeemed
Id ApplicationId AuthorizationId Ciphertext End Hash Start Status Subject Type
1 NULL NULL NULL 2017-10-12 11:24:26.0000000 +00:00 NULL 2017-09-12 11:24:26.0000000 +00:00 redeemed 1 refresh_token
And the response JSON doesn't include the new refresh token attribute.
I think for the second refresh, I thought at least one of the Start or End column should change since I configured to use sliding expiration.
But it is not the case. So I think there might be one issue with this refresh token method. Could you please have a look?
In the example: https://github.com/openiddict/openiddict-samples/tree/dev/samples/RefreshFlow
I downloaded and I can see each time when a refresh is done, a new token will be inserted, which is very different from the behaviour I had. BTW, I have changed the sample code to use slide expiration as well.
The main difference is my model is using int as TKey while the sample is using GUID. So I am wondering whether this is something to do with that?
options.UseOpenIddict<int>();
Found out the root cause for my issue:
// Create a new authentication ticket holding the user identity.
var ticket = new AuthenticationTicket(principal, *new AuthenticationProperties(),* OpenIdConnectServerDefaults.AuthenticationScheme);
While it should be:
// Create a new authentication ticket holding the user identity.
var ticket = new AuthenticationTicket(principal, properties, OpenIdConnectServerDefaults.AuthenticationScheme);

Login by multiple user accounts when using JSON Web Token

I am using JSON Web Token to authenticate the user in my application. Using https://jwt.io documentation, I use the jwt token to authenticate the user.
$tokenData = array(
"jti" => $tokenId,
"iat" => $issuedAt,
"exp" => $expire,
"user" => $user
);
$jwt = JWT::encode($tokenData, $secretKey, 'HS512');
But the problem is when one user can not login from multiple devices at the same time, it gives an error since the first login token get invaidated. Is there a way to handle this issue. Thanks in advance.

Google OpenIDConnect: Why am I not getting an 'openid_id' value along with 'sub'?

I've read all the documentation I can find on migrating from Google OpenID 2 to OAuth 2/OpenIDConnect, and am currently using a nice class from phpclasses.org . This class seems to work quite well with both Google and Facebook (haven't yet tried other providers), but I'm having a problem with just one aspect of Google's migration path that is quite critical to me: obtaining the google user's old OpenID identifier in addition to the new OpenIDConnect 'sub' value for that user. I've got users registered in my database only through their old OpenID identifiers.
According to Step 3 in Google's Migration Guide it looks like all I should need to do is add a parameter "openid.realm=http://www.example.com" to the authentication request sent to https://accounts.google.com/o/oauth2/auth.
I looked up in my old code what the realm was that I used for its OpenID registration process (it was 'http://' . $_SERVER['HTTP_HOST'];), and then I made sure that the redirect urls in my application were compatible with that realm.
I added that value (url-encoded) as the value of an openid.realm parameter passed on the authentication request made within the class. But when the class exchanged the token for an access token, it got back the correct email, name, sub, etc, but there was no openid_id parameter present. BTW, my scope parameter is 'openid email profile'
Does anyone have a suggestion for what else I should try, or what I can do to determine what the problem is? Does anyone have successful experience getting the openid_id parameter value in php code? I'd really rather not go the client-side route with their "Sign-in with Google" button, and according to the docs that really shouldn't be necessary (plus there's no particular reason to believe it would solve my problem if I did it).
Just discovered it's in the id_token returned along with the access_token when you exchange the authorization_code for the access_token.
In the Migration Document, Step 3 first two paragraphs:
When you send an OpenID Connect authentication request URI to Google
as described in Step 1, you include an openid.realm parameter. The
response that is sent to your redirect_uri includes an authorization
code that your application can use to retrieve an access token and an
ID token. (You can also retrieve an ID token directly from the OpenID
Connect authentication request by adding id_token to the response_type
parameter, potentially saving a back-end call to the token endpoint.)
The response from that token request includes the usual fields
(access_token, etc.), plus an openid_id field and the standard OpenID
Connect sub field. The fields you need in this context are openid_id
and sub:
This is confusing and misleading/wrong. What token request? The authentication request returns an authorization code that you can exchange for an access_token and an id_token. The parenthetical remark about adding id_token to the response_type doesn't help much, as the various ways I tried to do that resulted in an error. But in any event, the
"usual fields (access_token, etc.), plus an openid_id field..."
is wrong. The access_token never appears in the same list at the openid_id field. The access_token appears in a list with the id_token, and the openid_id field is encoded within the id_token!
For testing purposes, you can decode an id_token using https://www.googleapis.com/oauth2/v1/tokeninfo?id_token=<string>
In this documentation I couldn't find a useful description for how to decode an id_token, only caveats about their being sensitive, and how to validate them (though validation is not needed if obtained directly from a google endpoint as is the case here). I downloaded google's php client, and extracted code from it (src/Google/Auth/OAuth2.php and src/Google/Utils.php). And from that it's easy enough to figure out how to decode the id_token string: explode on ., base64_decode element 1, and json_decode that.
Update 2015-05-21: In reply to #Arthur's "answer", which would have been more appropriate as a comment on this answer. I would have commented on that answer myself, but comments aren't allowed to be very long and don't allow image uploads, plus I thought this extra info improves my answer...
Below is a screenshot from netbeans/xdebug, showing the array elements I get when decoding the id_token I get. Interesting that the intersection of the fields listed here with the fields listed by #Arthur is the null set. So I suspect that whatever #Arthur is decoding, it is not an id_token of the kind described here. I'm not familiar enough with this stuff even to guess what it is that's being decoded in that answer.
I'm afraid I don't have the time to dig through the library I use to extract the exact code path that produces the id_token I decoded to get this array using the simple algorithm I described. But I can tell you that the library I use is this: http://www.phpclasses.org/package/7700-PHP-Authorize-and-access-APIs-using-OAuth.html
Using it just as documented does not give you the id_token you need for this for two reasons:
The pre-configured server for Google with Oauth 2 doesn't handle the openid.realm parameter. To handle that, I added the following server definition to the oauth_configuration.json file:
"Google-OpenIdConnect":
{
"oauth_version": "2.0",
"dialog_url": "https://accounts.google.com/o/oauth2/auth?response_type=code&client_id={CLIENT_ID}&redirect_uri={REDIRECT_URI}&scope={SCOPE}&state={STATE}&openid.realm={REALM}",
"offline_dialog_url": "https://accounts.google.com/o/oauth2/auth?response_type=code&client_id={CLIENT_ID}&redirect_uri={REDIRECT_URI}&scope={SCOPE}&state={STATE}&access_type=offline&approval_prompt=force",
"access_token_url": "https://accounts.google.com/o/oauth2/token"
},
Just after the call to Initialize(), you need to add
$client->store_access_token_response = true;
Without that, the actual access_token response is not accessible (at least not the way I'm using the class). With those two changes in place, my exact code to get the openid_id using this class is as follows:
protected function jwt_decode($jwt) {
$segments = explode(".", $jwt);
if (count($segments) != 3) {
throw new Exception("Wrong number of segments in token: $jwt");
}
// Parse envelope.
$envelope = json_decode($this->urlSafeB64Decode($segments[0]), true);
if (!$envelope) {
throw new Exception("Can't parse token envelope: " . $segments[0]);
}
// Parse token
$json_body = $this->urlSafeB64Decode($segments[1]);
$payload = json_decode($json_body, true);
return $payload;
}
protected function getOpenid_id() {
require_once 'Phpclasses/Http/Class.php';
require_once 'Phpclasses/OauthClient/Class.php';
require 'Phpclasses/Google/private/keys.php';
$client = new oauth_client_class;
$client->configuration_file = $phpclasses_oauth_dir . '/oauth_configuration.json';
$client->server = 'Google-OpenIdConnect';
$client->redirect_uri = 'http://' . $_SERVER['HTTP_HOST'] . strtok($_SERVER['REQUEST_URI'], '?');
$client->client_id = $GOOGLE_APPID;
$client->client_secret = $GOOGLE_APPSECRET;
$client->scope = 'openid email';
$client->realm = $this->getRequest()->getScheme() . '://' . $this->getRequest()->getHttpHost();
$me = null;
if (($success = $client->Initialize())) {
// set *after* the call to Initialize
$client->store_access_token_response = true;
if (($success = $client->Process())) {
if (strlen($client->authorization_error)) {
$client->error = $client->authorization_error;
$success = false;
}
elseif (strlen($client->access_token)) {
$success = $client->CallAPI('https://www.googleapis.com/oauth2/v1/userinfo', 'GET', array(), array('FailOnAccessError' => true), $user);
$me = (array) $user;
if (!array_key_exists('id_token', $client->access_token_response)) {
throw new Exception('No id_token in \$client->access_token_response');
}
$openid_id = $this->jwt_decode($client->access_token_response['id_token']);
$me['openid_id'] = $openid_id;
}
}
$success = $client->Finalize($success);
}
if ($client->exit)
exit;
$client->ResetAccessToken();
if ($success) {
return $me;
}
// Code to handle failure...
}
Despite sootsnoot's (own) answer I still can't find the openid_id field anywhere. When decoding the id_token there are only "issuer", "issued_to", "audience", "user_id" , "expires_in" , "issued_at", "email" and "nonce" fields.
No "openid_id" field in sight..
Any ideas?
In response to sootsnoot's response :) And I apologize for not having enough reputation to comment, otherwise would have done so.
Am using an OpenID Connect library that takes endpoints from auto-config: https://accounts.google.com/.well-known/openid-configuration
So assume the endpoints are not the problem. Indeed it seems I was checking the wrong id_token. However, even when checking the correct one I still don't see the "openid_id" field. I now see everything you have, except that I have a "nonce" field instead of the "openid_id" field:
stdClass::__set_state(array( 'iss' => 'https://accounts.google.com', 'sub' => ****, 'azp' => ****, 'email' => ****, 'nonce' => ****, 'at_hash' => ****, 'email_verified' => true, 'aud' => ****, 'iat' => ****, 'exp' => 1432300788, ))
Must be doing something wrong, but what...
Final update:
Found the issue: was passing realm parameter as openid_realm=... instead of openid.realm=...
Oh do I feel stupid... :)

Refresh token giving invalid grant

I am running into an issue with one single user's refresh workflow for Google OAuth. I am correctly scoping for offline access and am storing that. Every 60 minutes, when needed, I retrieve a new access_token. Code has not changed, but what is odd is that when he first went through the authorization process it worked for about 3 days. Then we were running this issue, so I made him revoke access and go through the authorization again. This only lasted for 3 days once again.
client_id ="xxxxx.apps.googleusercontent.com"
client_secret ="yyyyyyyy"
refresh_token ="zzzzzzzz"
response = oauth2a.RefreshToken(client_id,client_secret,refresh_token)
def RefreshToken(client_id, client_secret, refresh_token):
params = {}
params['client_id'] = client_id
params['client_secret'] = client_secret
params['refresh_token'] = refresh_token
params['grant_type'] = 'refresh_token'
request_url = AccountsUrl('o/oauth2/token')
response = urllib.urlopen(request_url, urllib.urlencode(params)).read()
return json.loads(response)
The response is always {u'error': u'invalid_grant'}. I have attempted this on three different machines, so the NTP time sync is not the issue as well. All other user's refresh works fine. I am also never asking for a refresh_token again, so I know I'm not running into that 25 refresh_token limit. This is looking like it's a bug on the gmail side, is there any way that I can proceed to try to fix this?