-
Notifications
You must be signed in to change notification settings - Fork 74
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Adds user_secret validation for auth_token and refresh_token #80
base: develop
Are you sure you want to change the base?
Changes from all commits
0889ab2
57a202b
3fcf011
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -11,6 +11,8 @@ class Auth { | |
protected static $issued; | ||
protected static $expiration; | ||
protected static $is_refresh_token = false; | ||
protected static $refresh_token_valid_days = 30; | ||
protected static $auth_token_is_stateful = true; | ||
|
||
/** | ||
* This returns the secret key, using the defined constant if defined, and passing it through a filter to | ||
|
@@ -24,6 +26,7 @@ public static function get_secret_key() { | |
|
||
// Use the defined secret key, if it exists | ||
$secret_key = defined( 'GRAPHQL_JWT_AUTH_SECRET_KEY' ) && ! empty( GRAPHQL_JWT_AUTH_SECRET_KEY ) ? GRAPHQL_JWT_AUTH_SECRET_KEY : 'graphql-jwt-auth'; | ||
|
||
return apply_filters( 'graphql_jwt_auth_secret_key', $secret_key ); | ||
|
||
} | ||
|
@@ -64,6 +67,7 @@ public static function login_and_get_token( $username, $password ) { | |
*/ | ||
wp_set_current_user( $user->data->ID ); | ||
|
||
|
||
/** | ||
* The token is signed, now create the object with basic user data to send to the client | ||
*/ | ||
|
@@ -123,7 +127,7 @@ public static function get_token_expiration() { | |
/** | ||
* Retrieves validates user and retrieve signed token | ||
* | ||
* @param \WP_User $user Owner of the token. | ||
* @param \WP_User $user Owner of the token. | ||
* @param bool $cap_check Whether to check capabilities when getting the token | ||
* | ||
* @return null|string | ||
|
@@ -137,11 +141,27 @@ protected static function get_signed_token( $user, $cap_check = true ) { | |
return new \WP_Error( 'graphql-jwt-no-permissions', __( 'Only the user requesting a token can get a token issued for them', 'wp-graphql-jwt-authentication' ) ); | ||
} | ||
|
||
|
||
$secret = null; | ||
|
||
if( self::$auth_token_is_stateful || self::$is_refresh_token) { | ||
$secret = Auth::get_user_jwt_secret( $user->ID ); | ||
|
||
/** | ||
* Only allow access to a new token, if a valid user_secret is given and has not been revoked. | ||
*/ | ||
if ( empty( $secret ) || is_wp_error( $secret ) ) { | ||
return new \WP_Error( 'graphql-jwt-no-permissions', __( 'User secret of requesting user has been revoked or is missing.', 'wp-graphql-jwt-authentication' ) ); | ||
} | ||
} | ||
|
||
|
||
|
||
/** | ||
* Determine the "not before" value for use in the token | ||
* | ||
* @param string $issued The timestamp of the authentication, used in the token | ||
* @param \WP_User $user The authenticated user | ||
* @param string $issued The timestamp of the authentication, used in the token | ||
* @param \WP_User $user The authenticated user | ||
*/ | ||
$not_before = apply_filters( 'graphql_jwt_auth_not_before', self::get_token_issued(), $user ); | ||
|
||
|
@@ -156,15 +176,17 @@ protected static function get_signed_token( $user, $cap_check = true ) { | |
'exp' => self::get_token_expiration(), | ||
'data' => [ | ||
'user' => [ | ||
'id' => $user->data->ID, | ||
'id' => $user->data->ID, | ||
'user_secret' => $secret, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. At the moment, this is the one thing that makes a refresh token different than an access token. Having this in both tokens makes the tokens identical. |
||
], | ||
], | ||
]; | ||
|
||
|
||
/** | ||
* Filter the token, allowing for individual systems to configure the token as needed | ||
* | ||
* @param array $token The token array that will be encoded | ||
* @param array $token The token array that will be encoded | ||
* @param \WP_User $token The authenticated user | ||
*/ | ||
$token = apply_filters( 'graphql_jwt_auth_token_before_sign', $token, $user ); | ||
|
@@ -180,8 +202,8 @@ protected static function get_signed_token( $user, $cap_check = true ) { | |
* | ||
* For example, if the user should not be granted a token for whatever reason, a filter could have the token return null. | ||
* | ||
* @param string $token The signed JWT token that will be returned | ||
* @param int $user_id The User the JWT is associated with | ||
* @param string $token The signed JWT token that will be returned | ||
* @param int $user_id The User the JWT is associated with | ||
*/ | ||
$token = apply_filters( 'graphql_jwt_auth_signed_token', $token, $user->ID ); | ||
|
||
|
@@ -213,7 +235,7 @@ public static function get_user_jwt_secret( $user_id ) { | |
/** | ||
* Filter the capability that is tied to editing/viewing user JWT Auth info | ||
* | ||
* @param string 'edit_users' | ||
* @param string 'edit_users' | ||
* @param int $user_id | ||
*/ | ||
$capability = apply_filters( 'graphql_jwt_auth_edit_users_capability', 'edit_users', $user_id ); | ||
|
@@ -241,8 +263,8 @@ public static function get_user_jwt_secret( $user_id ) { | |
/** | ||
* Return the $secret | ||
* | ||
* @param string $secret The GraphQL JWT Auth Secret associated with the user | ||
* @param int $user_id The ID of the user the secret is associated with | ||
* @param string $secret The GraphQL JWT Auth Secret associated with the user | ||
* @param int $user_id The ID of the user the secret is associated with | ||
*/ | ||
return apply_filters( 'graphql_jwt_auth_user_secret', $secret, $user_id ); | ||
} | ||
|
@@ -302,8 +324,34 @@ public static function get_token( $user, $cap_check = true ) { | |
return self::get_signed_token( $user, $cap_check ); | ||
} | ||
|
||
/** | ||
* Filter the token signature, adding the user_secret to the signature and making the | ||
* expiration long lived if it is a refresh_token so that the token can be used for a long time without the client having to store a new | ||
* one. | ||
* | ||
* @param array $token Token. | ||
* @param \WP_User $user User associated with token. | ||
* | ||
* @return array $token | ||
*/ | ||
public static function increase_refresh_tokens_valid_days( $token, \WP_User $user ) { | ||
|
||
/** | ||
* Set the expiration date to $refresh_token_valid_days (days) from now to make the refresh token long lived, allowing the | ||
* token to be valid without changing as long as it has not been revoked or otherwise invalidated, | ||
* such as a refreshed user secret. | ||
*/ | ||
if ( self::$is_refresh_token ) { | ||
$token['exp'] = apply_filters( 'graphql_jwt_auth_refresh_token_expiration', ( self::get_token_issued() + ( DAY_IN_SECONDS * self::$refresh_token_valid_days ) ) ); | ||
self::$is_refresh_token = false; | ||
} | ||
|
||
return $token; | ||
} | ||
|
||
/** | ||
* Given a WP_User, this returns a refresh token for the user | ||
* | ||
* @param \WP_User $user A WP_User object | ||
* @param bool $cap_check | ||
* | ||
|
@@ -313,34 +361,12 @@ public static function get_refresh_token( $user, $cap_check = true ) { | |
|
||
self::$is_refresh_token = true; | ||
|
||
/** | ||
* Filter the token signature for refresh tokens, adding the user_secret to the signature and making the | ||
* expiration long lived so that the token can be used for a long time without the client having to store a new | ||
* one. | ||
*/ | ||
add_filter( 'graphql_jwt_auth_token_before_sign', function( $token, \WP_User $user ) { | ||
$secret = Auth::get_user_jwt_secret( $user->ID ); | ||
|
||
if ( ! empty( $secret ) && ! is_wp_error( $secret ) && true === self::is_refresh_token() ) { | ||
|
||
/** | ||
* Set the expiration date as a year from now to make the refresh token long lived, allowing the | ||
* token to be valid without changing as long as it has not been revoked or otherwise invalidated, | ||
* such as a refreshed user secret. | ||
*/ | ||
$token['exp'] = apply_filters( 'graphql_jwt_auth_refresh_token_expiration', ( self::get_token_issued() + ( DAY_IN_SECONDS * 365 ) ) ); | ||
$token['data']['user']['user_secret'] = $secret; | ||
|
||
self::$is_refresh_token = false; | ||
|
||
} | ||
|
||
return $token; | ||
}, 10, 2 ); | ||
add_filter( 'graphql_jwt_auth_token_before_sign', [ __CLASS__, 'increase_refresh_tokens_valid_days' ], 10, 2 ); | ||
|
||
return self::get_signed_token( $user, $cap_check ); | ||
} | ||
|
||
|
||
public static function is_refresh_token() { | ||
return true === self::$is_refresh_token ? true : false; | ||
} | ||
|
@@ -432,7 +458,7 @@ public static function revoke_user_secret( int $user_id ) { | |
/** | ||
* Filter the capability that is tied to editing/viewing user JWT Auth info | ||
* | ||
* @param string 'edit_users' | ||
* @param string 'edit_users' | ||
* @param int $user_id | ||
*/ | ||
$capability = apply_filters( 'graphql_jwt_auth_edit_users_capability', 'edit_users', $user_id ); | ||
|
@@ -476,7 +502,7 @@ public static function unrevoke_user_secret( int $user_id ) { | |
/** | ||
* Filter the capability that is tied to editing/viewing user JWT Auth info | ||
* | ||
* @param string 'edit_users' | ||
* @param string 'edit_users' | ||
* @param int $user_id | ||
*/ | ||
$capability = apply_filters( 'graphql_jwt_auth_edit_users_capability', 'edit_users', $user_id ); | ||
|
@@ -505,9 +531,9 @@ public static function unrevoke_user_secret( int $user_id ) { | |
|
||
|
||
protected static function set_status( $status_code ) { | ||
add_filter( 'graphql_response_status_code', function() use ( $status_code ) { | ||
add_filter( 'graphql_response_status_code', function () use ( $status_code ) { | ||
return $status_code; | ||
}); | ||
} ); | ||
} | ||
|
||
/** | ||
|
@@ -516,8 +542,8 @@ protected static function set_status( $status_code ) { | |
* | ||
* @param string $token The encoded JWT Token | ||
* | ||
* @throws \Exception | ||
* @return mixed|boolean|string | ||
* @throws \Exception | ||
*/ | ||
public static function validate_token( $token = null, $refresh = false ) { | ||
|
||
|
@@ -571,7 +597,7 @@ public static function validate_token( $token = null, $refresh = false ) { | |
JWT::$leeway = 60; | ||
|
||
$secret = self::get_secret_key(); | ||
$token = ! empty( $token ) ? JWT::decode( $token, $secret, [ 'HS256' ] ) : null; | ||
$token = ! empty( $token ) ? JWT::decode( $token, $secret, [ 'HS256' ] ) : null; | ||
|
||
/** | ||
* The Token is decoded now validate the iss | ||
|
@@ -588,21 +614,31 @@ public static function validate_token( $token = null, $refresh = false ) { | |
} | ||
|
||
/** | ||
* If there is a user_secret in the token (refresh tokens) make sure it matches what | ||
* If there is a user_secret in the token make sure it is not revoked and still valid. | ||
*/ | ||
if ( isset( $token->data->user->user_secret ) ) { | ||
|
||
if ( Auth::is_jwt_secret_revoked( $token->data->user->id ) ) { | ||
throw new \Exception( __( 'The User Secret does not match or has been revoked for this user', 'wp-graphql-jwt-authentication' ) ); | ||
throw new \Exception( __( 'The User Secret has been revoked for this user', 'wp-graphql-jwt-authentication' ) ); | ||
} | ||
|
||
$token_user_secret = $token->data->user->user_secret; | ||
$user_secret = get_user_meta( $token->data->user->id, 'graphql_jwt_auth_secret', true ); | ||
|
||
if ( $token_user_secret !== $user_secret ) { | ||
throw new \Exception( __( 'The User Secret does not match for this user', 'wp-graphql-jwt-authentication' ) ); | ||
} | ||
} else if (self::$auth_token_is_stateful || self::$is_refresh_token) { | ||
throw new \Exception( __( 'The User Secret is missing in the token.', 'wp-graphql-jwt-authentication' ) ); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same comment as above. If the secret exists for both auth tokens and refresh tokens, the tokens are the same 🤔 |
||
} | ||
|
||
/** | ||
* If any exceptions are caught | ||
*/ | ||
} catch ( \Exception $error ) { | ||
self::set_status( 403 ); | ||
return new \WP_Error( 'invalid_token', __( 'The JWT Token is invalid', 'wp-graphql-jwt-authentication' ) ); | ||
|
||
return new UserError( __( 'The JWT Token is invalid: ' . $error, 'wp-graphql-jwt-authentication' ) ); | ||
} | ||
|
||
self::$is_refresh_token = false; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,6 @@ | ||
acceptance.suite.yml | ||
functional.suite.yml | ||
wpunit.suite.yml | ||
unit.suite.yml | ||
unit.suite.yml | ||
* | ||
!.gitignore |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think 30 days is probably a fine default instead of a year. If you're not using an application for more than 30 days, the application asking you to login again seems reasonable.