JWT refresh token. Can it be self contained? - security

I want to keep tokens like this (payload):
access_token
{
"user": "john_doe",
"iat": 1444262543,
"exp": 1444262563, // 15 minutes
"type": "access"
}
refresh_token
{
"user": "john_doe",
"iat": 1444262543,
"exp": 1444262563, // 24 hours
"type": "refresh"
}
As you can see access_token and refresh_token are almost identical except of lifetime and type.
Is this a problem? Can it be security hole when using Refresh token this way?
PS: The reason behind this is because I don't want to store refresh token in a storage (DB/Redis).

From security perspective, self contained refresh tokens are vulnerable.
The reason we have refresh token is to extend the validity of the access token. In other words, we hit an Authorization server (or have a different flow altogether for authorization), verify it using some public key and get the access token from the service (created using private key).
A refresh token HAS to be stored on the server side. We shouldn't leverage the "self-contained" property of JWT for a refresh token. Doing so leaves us with no way to revoke refresh tokens other than changing our private key.
Example: Suppose i'm logged into your application from my cellphone and it's lost.
The application contains the refresh token right, and that contains all my information. If refresh_token is not stored at the backend, we do not have any way to invalidate that session. Now the private key which creates the token needs to be changed, which is not good.
The higher level steps to create access_token from refresh_token could be something like this:
Let's lay the groundwork first:
The tokens table in the database could contain the following fields:
user_id<uuid>: id of the user
access_token<text>: access token
access_token_expiry<date>: access token expiring timestamp
refresh_token<text>: refresh token (sha)
refresh_token_expiry<date>: refresh token expiring timestamp
refresh_count: number of times the refresh token has been used to access access_token (just in case if you need this field)
Note that we should store the hash of the refresh token in the database.
Also, one user could have multiple sessions, say they're logged into our application from different browsers on the same device / from different devices. To cater to this requirement, we need to create one login_sessions table
id<BIGINT>: primary key for the table
user_id<uuid>: id of the user who logged in
map_id<uuid>: This will be the key which maps one access token to its refresh token counterpart. Both tokens of the pair will contain this id in their body.
status<String>: could be ACTIVE|INACTIVE(logged out, INACTIVE user will not be allowed to get access_token from the refresh_token)
created_on<Date>: timestamp for record created on
modified_on<Date>: timestamp for record modified on
refresh_token body:
{
"iss": "ABC Service", (The Issuer of the token)
"sub": "4953fag3-ec5e-4ed3-b09e-d847f3f376c6", (The user_id, subject of the token)
"aud": "UI", (audience who will use the token)
"typ": "refresh", (type)
"iat": 1599326501,
"exp": 1601918501,
"jti": "749c77e5-bac0-43f1-aeea-1618ada0224f", (unique identifier for this token)
"mid": "e392692b-6d77-49a9-9928-ac3c3d5208a3" (unique identifier for access-refresh token pair - refers to map_id in login_sessions table)
}
access_token body:
{
"iss": "ABC Service", (The Issuer of the token)
"sub": "4953fag3-ec5e-4ed3-b09e-d847f3f376c6", (The user_id, subject of the token)
"aud": "UI", (audience who will use the token)
"typ": "access", (type)
"iat": 1599326501,
"exp": 1599412901,
"jti": "3d2985ef-e767-495e-af88-448fc0ecb167", (unique identifier for this token)
"mid": "e392692b-6d77-49a9-9928-ac3c3d5208a3", (unique identifier for access-refresh token pair - refers to map_id in login_sessions table)
}
"fname": "john",
"lname": "doe",
"roles": [99b18377-5b4c-4e68-8ff4-bac4aea93bd2]
"other_key": "other_value"
Generating access token from refresh token:
Once access token is expired, we hit authorization server api to get the access token, while passing the refresh_token (jwt) in the body.
The authorization server will have this API exposed which will accept refresh token and perform the following steps and then return access token.
Steps:
1. Verify the refresh_token with public key (whose counterpart private formed the token initially)
2. Pick the mid(map_id) from the token body
3. Get login session record containing this mid from the login_sessions table. Go to next step if there exists some login_session record.
4. If the status of the login_session record is ACTIVE, create one access_token (using the private key) with relevant details in it's body
5. Send back the access token as a response.
If any of the steps 1-4 fail above, we send back an error response with relevant message.

Related

Why is the Google OAuth 2.0 flow not returning an id_token when exchanged with the authorization code?

There's a clear diagram of the how this process works in Google's OAuth 2.0 docs. I've implemented the diagram as expected. My app hasn't been verified by Google yet, so the OAuth consent screen shows a warning and only allows whitelisted emails to be used in the OAuth process (until the app is verified). Using my email addresses, the code works perfectly. As soon as I started using some friends' emails for testing (all of which were whitelisted in the Google Developer Console), I'm not receiving the id_token. Interestingly, I'm not seeing any errors. Exchanging the code, I'm still receiving the access_token and refresh_token but not the id_token.
I don't think it's a problem with my code since I receive the id_token when using my personal email accounts, but here's my code anyways.
class GoogleOauthService implements GoogleOauth {
#client;
constructor() {
this.#client = new google.auth.OAuth2(
process.env.GOOGLE_CLIENT_ID,
process.env.GOOGLE_CLIENT_SECRET,
process.env.GOOGLE_REDIRECT_URL
);
}
async getTokens(
params: GoogleOauth.GetTokensParams
): GoogleOauth.GetTokensResult {
const { tokens } = await this.#client.getToken(params.code);
return {
accessToken: tokens.access_token,
refreshToken: tokens.refresh_token,
scope: tokens.scope,
tokenType: tokens.token_type,
idToken: tokens.id_token,
expiryDate: tokens.expiry_date,
};
}
}
I'm not receiving any errors from the call. tokens results in the following:
{
access_token: 'secret access token',
refresh_token: 'secret refresh token',
scope: 'https://www.googleapis.com/auth/calendar',
token_type: 'Bearer',
expiry_date: 1673742003175
}
If anyone has any idea why Google isn't giving the id_token for some emails, I'd appreciate the insight.
The issue you were having was related to your scope of authorization you are not requesting the proper ones.
Here is a little background information on why you needed to change your scope.
Oauth2 is used for authorization, so if you want to request permission from a user to access their private user data.
I can see that you are requesting the scope of https://www.googleapis.com/auth/calendar You are using Oauth2 to request permission of a user to access their google calendar data this is Oauth2. The response will be an access token granting you access to their google calendar data and a refresh token. This is what Oauth2 responds with
{
access_token: 'secret access token',
refresh_token: 'secret refresh token',
scope: 'https://www.googleapis.com/auth/calendar',
token_type: 'Bearer',
expiry_date: 1673742003175
}
Then we have Sign-in or authentication or Open Id connect which was actually built on top of Oauth2. Open Id connect allows you to verify that the user behind the machine is in fact the owner of the account. This is authencation not authorization. We are authenticating or signing in a user. Open Id connect returns an access token and a refresh token and an id token.
{
"access_token": "[redacted]",
"id_token": "[redacted]",
"expires_in": 3599,
"token_type": "Bearer",
"scope": "openid https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile",
"refresh_token": "[redacted]"
}
To use open id connect you need to use the profile, openid, and/ or email scopes. Then you will get back an access token granting you access to use profile information as well as the refrshtoken, and also an id token.
The id token is the key part of open id connect it gives you information on the user itself if you take it over to jwt.io you can decrypt the jwt.
{
"iss": "https://accounts.google.com",
"azp": "407408718192.apps.googleusercontent.com",
"aud": "407408718192.apps.googleusercontent.com",
"sub": "[MyInternalGoogleId]",
"email": "[MyEmailAddress]",
"email_verified": true,
"at_hash": "Je-vk1k8MRaqK0gw3yXj7g",
"name": "Linda Lawton",
"picture": "https://lh3.googleusercontent.com/a/AEdFTp46WTOIbHj5Nl1VJRFf4Izj9YrX61i75F16H1qSA1Q=s96-c",
"given_name": "Linda",
"family_name": "Lawton",
"locale": "en",
"iat": 1673784532,
"exp": 1673788132
}
The Id token allows you to verify after a user has been authenticated that they are in fact the owner of the account and just signed in.
as opposed to authorization which will only give you an access token with no guarantee that the one sending the access token was the user who owns the account, it could be an automated server app which has been granted authorization to access the users data.
I'm now able to retrieve the id_token for accounts that I was previously unable to. The change I made was including the following in my OAuth 2.0 scopes
[
'https://www.googleapis.com/auth/userinfo.profile',
'https://www.googleapis.com/auth/userinfo.email',
];
Once these two scopes were included, the id_token appeared.

JWT Revoke Refresh Token

I'm building a Node.js API service. I'm issuing a JWT access token that expires every 5 minutes, a refresh token that expires in a month, and /fetch-access route to get a new access token when called from the SPA/client.
Now I'm using a database to store a list of recently revoked refresh tokens that an administrative user can populate by some administrative action (i.e. "suspend account", "change permissions", etc.).
My assumptions were that I would produce the same JWT token string value, store that in the database and query the database for that value when the /fetch-access route is called. However, what I realize now is that; at the time that the administrative user is revoking the JWT, I have no way to replicate the same JWT string value at that point in time.
To illustrate my point;
const jwt = require('jsonwebtoken')
let refresh_token_on_login = jwt.sign({user: 'bob'}, 'secret123', {expiresIn: '7d'})
let refresh_token_on_revoke = jwt.sign({user: 'bob'}, 'secret123', {expiresIn: '7d'})
console.log(refresh_token_on_login)
//eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0ZXN0Ijp0cnVlLCJpYXQiOjE2MDA4Mzg2NzUsImV4cCI6MTYwMTQ0MzQ3NX0.X2zBWVr5t3olb5GsPebHULh-j1-iiuyjJmb98jzlZ2Q
console.log(refresh_token_on_revoke)
//eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0ZXN0Ijp0cnVlLCJpYXQiOjE2MDA4Mzg3MTMsImV4cCI6MTYwMTQ0MUxM30.4LDBJv4qrLLzOieDtyXu8vWqJ1EY75vShpUWiX7jepQgg
In order for the administrator to invalidate refresh token that was issued to the user at a prior time, the revoke key cannot be the actual token string, but some other value that gets saved somewhere in the database at the time of login (i.e. UserModel = { last_issued_refresh: 'xyz'}). That same value needs to be embedded in the refresh token itself to be used in checking the black-listed tokens upon /fetch-access requests.
Are my assumptions correct? Is this the only way to do this, or am I missing some key point in how the refresh token gets revoked (by a user other than the client user - such as admin)

Cross-Domain link authentication using JWT

I have a link that contains a parameter token which is the JWT token that will be validated across another domain. I'm struggling with the fact that a user can copy and share that link across several browsers or with other users.
The jwt payload looks like the following:
{
"userId": "46",
"role": "User",
"nbf": 1589877059,
"exp": 1589963459,
"iat": 1589877059
}
My goal is to secure that link from being shared other than the current user while authenticating that user on the second domain.
Am i missing something or is there any other way to secure a link across different domains? Any suggestion is really appreciated.
Update
An approach I'm trying is storing the last active SessionId in the database and validating it on both sides. In case the token is valid and actual SessionId is different from the last active id, then redirect to some unauthorized page. but the drawback is that the user will require session-authentication instead of cookie authentication since the session will reset each time the user closes the browser but the cookie will remain the same.

How to create a JWT that contains the Auth0 User_Metadata

So I know how to create tokens and how to read tokens but I am running into an issue with getting the User_Metadata from the Users I created in Auth0 (without login them in from my application).
What I am trying to do is this:
User some where with a device logs into Auth0 and generates a JWT
Token
User now calls my API and passes Bearer with token in header
I read Bearer and Authenticate that the token is good. I then want to
pull the user information from the token to use to make sure they
have rights to do something.
I am not wanting a 2nd database that holds user information that they will need to log into my API so I know who they are. I just want to be able to use the JWT Token to get that information. Right now when I create a token I have this in the Payload:
{
"iss": "https://.....",
"sub": "RTMLeICuyL1kyeQN#clients",
"aud": "https://.....",
"exp": 1494031764,
"iat": 1493945364,
"scope": ""
}
If I go to Auth0 User Details tab I can see the user and the user_metadata and app_metadata that I want to return but not sure how to get it. Thanks for any help.
I was not able to get the User Profile data from Auth0 to come in on the JWT Token but I was able to use the client scopes in Auth0 to create the scopes needed to do Authorization. This is still not the best answer but it will allow me to determine if someone has rights to view that object and records.
I will go into more details as I write up an example.

Why is my request for a new access token not returning a new refresh token?

I am using the following code, along with my refresh token, to request a new access token:
exports.getTokenFromRefreshToken = function (user, callback) {
request.post({
url:'https://login.microsoftonline.com/12345678-1234-1234-1234-2f189712345/oauth2/token',
form: {
grant_type: 'refresh_token',
refresh_token: refresh_token,
client_id: client_id,
client_secret: client_secret,
resource: 'https://graph.microsoft.com'
}
}, function(err, httpResponse, body) {
if (!err) {
var tokens = JSON.parse(httpResponse.body);
console.log('getTokenFromRefreshToken() tokens = ' + JSON.stringify(tokens));
callback(null, tokens);
}
})
};
The httpResponse includes everything that I get when I make the original token request (from the code), but without a new refresh token. I was under the impression that I would also receive a new refresh token. Is that not the case?
You get a new refresh token only when you are including the offline_access scope.
ref.: https://azure.microsoft.com/en-us/documentation/articles/active-directory-v2-scopes/
The offline_access scope gives your app access to resources on behalf of the user for an extended time. On the work account consent page, this scope appears as the "Access your data anytime" permission. On the personal Microsoft account consent page, it appears as the "Access your info anytime" permission. When a user approves the offline_access scope, your app can receive refresh tokens from the v2.0 token endpoint. Refresh tokens are long-lived. Your app can get new access tokens as older ones expire.
Refresh tokens aren't refreshed the same way you can get a new access token using the refresh token. When a refresh token expires, you will need to need to get the credentials and do the initial token acquisition again.
More info here: Refreshing an Access Token
It looks like it should work except you seem to be missing the redirect URI. I have a working version of this call but it includes this redirect_uri. It produces for me both a new access and refresh token.
--
http://graph.microsoft.io/en-us/docs/authorization/app_authorization
Renew expiring access token using refresh token
The redirect URL that the browser is sent to when authentication is complete. This should match the redirect_uri value used in the first request.
I had the exact same issue, caused a headache for a while until the problem was found.
Seems like you are probably logging in with a Guest MS account (previously known as Live) and thus getting a 12hr expiry refresh token with no rolling window.
You need to use a full MS account to get the refresh token back in the response body (which is a token that will last 14 days), with a rolling window of 90 days.
For as long as you use a Guest MSA you will not get a refresh token back. Your code is correctly structured as far as I can see, and I don't believe you need a redirect_uri header as stated above, they are optional fields according to doco.
For more information on the types of refresh tokens, see this blog post:
Azure tokens

Resources