Skip to content

Commit

Permalink
Merge pull request #81 from wp-graphql/release/v0.4.0
Browse files Browse the repository at this point in the history
Release/v0.4.0
  • Loading branch information
jasonbahl authored Feb 20, 2020
2 parents d77da3f + 827f263 commit e65055d
Show file tree
Hide file tree
Showing 36 changed files with 186 additions and 7,326 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ vendor/*
c3.php
!/tests
/tests/*.suite.yml
/tests/_output
.env
.codeception.yml
composer.lock
65 changes: 54 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ This plugin was initially based off the `wp-api-jwt-auth` plugin by Enrique Chav

## Install, Activate & Setup

You can install and activate the plugin like any WordPress plugin. Download the .zip from Github and add to your plugins directory, then activate.
You can install and activate the plugin like any WordPress plugin. Download the .zip from Github and add to your plugins directory, then activate.

JWT uses a Secret defined on the server to validate the signing of tokens.
JWT uses a Secret defined on the server to validate the signing of tokens.

It's recommended that you use something like the WordPress Salt generator (https://api.wordpress.org/secret-key/1.1/salt/) to generate a Secret.

Expand All @@ -25,7 +25,7 @@ You can define a Secret like so:
define( 'GRAPHQL_JWT_AUTH_SECRET_KEY', 'your-secret-token' );
```

Or you can use the filter `graphql_jwt_auth_secret_key` to set a Secret like so:
Or you can use the filter `graphql_jwt_auth_secret_key` to set a Secret like so:

```
add_filter( 'graphql_jwt_auth_secret_key', function() {
Expand All @@ -51,15 +51,19 @@ For NGINX, this may work: https://serverfault.com/questions/511206/nginx-forward

## How the plugin Works

This plugin adds a new `login` mutation to the WPGraphQL Schema.
### Login User

This can be used like so:
This plugin adds a new `login` mutation to the WPGraphQL Schema.

```
This can be used like so:

**Input-Type:** `LoginUserInput!`

```graphql
mutation LoginUser {
login( input: {
clientMutationId:"uniqueId"
username: "your_login"
clientMutationId: "uniqueId",
username: "your_login",
password: "your password"
} ) {
authToken
Expand All @@ -71,13 +75,52 @@ mutation LoginUser {
}
```

The `authToken` that is received in response to the login mutation can then be stored in local storage (or similar) and
used in subsequent requests as an HTTP Authorization header to Authenticate the user prior to execution of the
GraphQL request.
The `authToken` that is received in response to the login mutation can then be stored in local storage (or similar) and
used in subsequent requests as an HTTP Authorization header to Authenticate the user prior to execution of the
GraphQL request.

- **Set authorization header in Apollo Client**: https://www.apollographql.com/docs/react/networking/authentication/#header
- **Set authorization header in Relay Modern**: https://relay.dev/docs/en/network-layer.html
- **Set authorization header in Axios**: https://github.com/axios/axios#axioscreateconfig


### Register User

**Input-Type:** `RegisterUserInput!`

```graphql
mutation RegisterUser {
registerUser(
input: {
clientMutationId: "uniqueId",
username: "your_username",
password: "your_password",
email: "your_email"
}) {
user {
jwtAuthToken
jwtRefreshToken
}
}
}
```

### Refresh Auth Token

**Input-Type:** `RefreshJwtAuthTokenInput!`

```graphql
mutation RefreshAuthToken {
refreshJwtAuthToken(
input: {
clientMutationId: "uniqueId"
jwtRefreshToken: "your_refresh_token",
}) {
authToken
}
}
```


## Example using GraphiQL
![Example using GraphiQL](https://github.com/wp-graphql/wp-graphql-jwt-authentication/blob/master/img/jwt-auth-example.gif?raw=true)
103 changes: 61 additions & 42 deletions src/Auth.php
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,8 @@ public static function login_and_get_token( $username, $password ) {
* The token is signed, now create the object with basic user data to send to the client
*/
$response = [
'authToken' => self::get_signed_token( $user ),
'refreshToken' => self::get_refresh_token( $user ),
'authToken' => self::get_signed_token( wp_get_current_user() ),
'refreshToken' => self::get_refresh_token( wp_get_current_user() ),
'user' => DataSource::resolve_user( $user->data->ID, \WPGraphQL::get_app_context() ),
'id' => $user->data->ID,
];
Expand Down Expand Up @@ -123,7 +123,8 @@ public static function get_token_expiration() {
/**
* Retrieves validates user and retrieve signed token
*
* @param User|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
*/
Expand Down Expand Up @@ -200,11 +201,13 @@ protected static function get_signed_token( $user, $cap_check = true ) {
*/
public static function get_user_jwt_secret( $user_id ) {

$is_revoked = Auth::is_jwt_secret_revoked( $user_id );

/**
* If the secret has been revoked, throw an error
*/
if ( true === Auth::is_jwt_secret_revoked( $user_id ) ) {
return new \WP_Error( 'graphql-jwt-revoked-secret', __( 'The JWT Auth secret cannot be returned', 'wp-graphql-jwt-authentication' ) );
if ( true === (bool) $is_revoked ) {
return null;
}

/**
Expand All @@ -216,11 +219,11 @@ public static function get_user_jwt_secret( $user_id ) {
$capability = apply_filters( 'graphql_jwt_auth_edit_users_capability', 'edit_users', $user_id );

/**
* If the request is not from the current_user AND the current_user doesn't have the proper capabilities, don't return the secret
* If the request is not from the current_user or the current_user doesn't have the proper capabilities, don't return the secret
*/
$is_current_user = ( $user_id === get_current_user_id() ) ? true : false;
if ( ! $is_current_user && ! current_user_can( $capability ) ) {
return new \WP_Error( 'graphql-jwt-improper-capabilities', __( 'The JWT Auth secret for this user cannot be returned', 'wp-graphql-jwt-authentication' ) );
return null;
}

/**
Expand All @@ -232,7 +235,7 @@ public static function get_user_jwt_secret( $user_id ) {
* If there is no stored secret, or it's not a string
*/
if ( empty( $secret ) || ! is_string( $secret ) ) {
Auth::issue_new_user_secret( $user_id );
$secret = Auth::issue_new_user_secret( $user_id );
}

/**
Expand Down Expand Up @@ -291,13 +294,21 @@ public static function is_jwt_secret_revoked( $user_id ) {
* Public method for getting an Auth token for a given user
*
* @param \WP_USer $user The user to get the token for
* @param boolean $cap_check Whether to check capabilities. Default is true.
*
* @return null|string
*/
public static function get_token( $user, $cap_check = true ) {
return self::get_signed_token( $user, $cap_check );
}

/**
* Given a WP_User, this returns a refresh token for the user
* @param \WP_User $user A WP_User object
* @param bool $cap_check
*
* @return null|string
*/
public static function get_refresh_token( $user, $cap_check = true ) {

self::$is_refresh_token = true;
Expand All @@ -309,6 +320,7 @@ public static function get_refresh_token( $user, $cap_check = true ) {
*/
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() ) {

/**
Expand Down Expand Up @@ -415,7 +427,7 @@ public static function filter_determine_current_user( $user ) {
*
* @return mixed|boolean|\WP_Error
*/
public static function revoke_user_secret( int $user_id ) {
public static function revoke_user_secret( $user_id ) {

/**
* Filter the capability that is tied to editing/viewing user JWT Auth info
Expand Down Expand Up @@ -530,7 +542,7 @@ public static function validate_token( $token = null, $refresh = false ) {
* @since 0.0.1
*/
if ( empty( $auth_header ) ) {
return false;
return $token;
} else {
/**
* The HTTP_AUTHORIZATION is present verify the format
Expand All @@ -545,52 +557,59 @@ public static function validate_token( $token = null, $refresh = false ) {
* If there's no secret key, throw an error as there needs to be a secret key for Auth to work properly
*/
if ( ! self::get_secret_key() ) {
throw new \Exception( __( 'JWT is not configured properly', 'wp-graphql-jwt-authentication' ) );
self::set_status( 403 );
return new \WP_Error( 'invalid-secret-key', __( 'JWT is not configured properly', 'wp-graphql-jwt-authentication' ) );
}



/**
* Try to decode the token
* Decode the Token
*/
try {
JWT::$leeway = 60;

/**
* Decode the Token
*/
JWT::$leeway = 60;
$secret = self::get_secret_key();

$secret = self::get_secret_key();
try {
$token = ! empty( $token ) ? JWT::decode( $token, $secret, [ 'HS256' ] ) : null;
} catch ( \Exception $exception ) {
$token = new \WP_Error( 'invalid-secret-key', $exception->getMessage() );
}

/**
* The Token is decoded now validate the iss
*/
if ( ! isset( $token->iss ) || get_bloginfo( 'url' ) !== $token->iss ) {
throw new \Exception( __( 'The iss do not match with this server', 'wp-graphql-jwt-authentication' ) );
}
/**
* If there's no token listed, just bail now before validating an empty token.
* This will treat the request as a public request
*/
if ( empty( $token ) ) {
return $token;
}

/**
* So far so good, validate the user id in the token
*/
if ( ! isset( $token->data->user->id ) ) {
throw new \Exception( __( 'User ID not found in the token', 'wp-graphql-jwt-authentication' ) );
}
/**
* The Token is decoded now validate the iss
*/
if ( ! isset( $token->iss ) || get_bloginfo( 'url' ) !== $token->iss ) {
return new \WP_Error( 'invalid-jwt', __( 'The iss do not match with this server', 'wp-graphql-jwt-authentication' ) );
}

/**
* If there is a user_secret in the token (refresh tokens) make sure it matches what
*/
if ( isset( $token->data->user->user_secret ) ) {
/**
* So far so good, validate the user id in the token
*/
if ( ! isset( $token->data->user->id ) ) {
return new \WP_Error( 'invalid-jwt', __( 'User ID not found in the token', 'wp-graphql-jwt-authentication' ) );
}

/**
* If there is a user_secret in the token (refresh tokens) make sure it matches what
*/
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' ) );
}
if ( Auth::is_jwt_secret_revoked( $token->data->user->id ) ) {
return new \WP_Error( 'invalid-jwt', __( 'The User Secret does not match or has been revoked for this user', 'wp-graphql-jwt-authentication' ) );
}
}

/**
* If any exceptions are caught
*/
} catch ( \Exception $error ) {
if ( is_wp_error( $token ) ) {
self::set_status( 403 );
return new \WP_Error( 'invalid_token', __( 'The JWT Token is invalid', 'wp-graphql-jwt-authentication' ) );
}

self::$is_refresh_token = false;
Expand Down
Loading

0 comments on commit e65055d

Please sign in to comment.