Code Coverage
 
Classes and Traits
Functions and Methods
Lines
Total
0.00% covered (danger)
0.00%
0 / 1
57.89% covered (warning)
57.89%
11 / 19
CRAP
76.99% covered (warning)
76.99%
87 / 113
WPGraphQL\JWT_Authentication\Auth
0.00% covered (danger)
0.00%
0 / 1
61.90% covered (warning)
61.90%
13 / 21
140.70
76.99% covered (warning)
76.99%
87 / 113
 get_secret_key
100.00% covered (success)
100.00%
1 / 1
3
100.00% covered (success)
100.00%
2 / 2
 login_and_get_token
0.00% covered (danger)
0.00%
0 / 1
4.13
80.00% covered (warning)
80.00%
8 / 10
 get_token_issued
100.00% covered (success)
100.00%
1 / 1
2
100.00% covered (success)
100.00%
3 / 3
 get_token_expiration
100.00% covered (success)
100.00%
1 / 1
3
100.00% covered (success)
100.00%
4 / 4
 get_signed_token
0.00% covered (danger)
0.00%
0 / 1
5.01
92.31% covered (success)
92.31%
12 / 13
 get_user_jwt_secret
0.00% covered (danger)
0.00%
0 / 1
7.39
80.00% covered (warning)
80.00%
8 / 10
 issue_new_user_secret
100.00% covered (success)
100.00%
1 / 1
3
100.00% covered (success)
100.00%
5 / 5
 is_jwt_secret_revoked
100.00% covered (success)
100.00%
1 / 1
3
100.00% covered (success)
100.00%
2 / 2
 get_token
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
1 / 1
 get_refresh_token
100.00% covered (success)
100.00%
1 / 1
3
n/a
0 / 0
 anonymousFunction:305#1277
100.00% covered (success)
100.00%
1 / 1
3
100.00% covered (success)
100.00%
6 / 6
 is_refresh_token
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 1
 authenticate_user
0.00% covered (danger)
0.00%
0 / 1
5.02
60.00% covered (warning)
60.00%
3 / 5
 filter_determine_current_user
100.00% covered (success)
100.00%
1 / 1
4
100.00% covered (success)
100.00%
5 / 5
 revoke_user_secret
0.00% covered (danger)
0.00%
0 / 1
20
0.00% covered (danger)
0.00%
0 / 7
 unrevoke_user_secret
0.00% covered (danger)
0.00%
0 / 1
12
0.00% covered (danger)
0.00%
0 / 6
 set_status
100.00% covered (success)
100.00%
1 / 1
1
n/a
0 / 0
 anonymousFunction:485#2020
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
2 / 2
 validate_token
0.00% covered (danger)
0.00%
0 / 1
12.24
78.26% covered (warning)
78.26%
18 / 23
 get_auth_header
100.00% covered (success)
100.00%
1 / 1
5
100.00% covered (success)
100.00%
4 / 4
 get_refresh_header
100.00% covered (success)
100.00%
1 / 1
2
100.00% covered (success)
100.00%
2 / 2
<?php
namespace WPGraphQL\JWT_Authentication;
use Firebase\JWT\JWT;
use GraphQL\Error\UserError;
use WPGraphQL\Data\DataSource;
class Auth {
    protected static $issued;
    protected static $expiration;
    protected static $is_refresh_token = false;
    /**
     * This returns the secret key, using the defined constant if defined, and passing it through a filter to
     * allow for the config to be able to be set via another method other than a defined constant, such as an
     * admin UI that allows the key to be updated/changed/revoked at any time without touching server files
     *
     * @return mixed|null|string
     * @since 0.0.1
     */
    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 );
    }
    /**
     * Get the user and password in the request body and generate a JWT
     *
     * @param string $username
     * @param string $password
     *
     * @return mixed
     * @throws \Exception
     * @since 0.0.1
     */
    public static function login_and_get_token( $username, $password ) {
        /**
         * First thing, check the secret key if not exist return a error
         */
        if ( empty( self::get_secret_key() ) ) {
            throw new UserError( __( 'JWT Auth is not configured correctly. Please contact a site administrator.', 'wp-graphql-jwt-authentication' ) );
        }
        /**
         * Authenticate the user and get the Authenticated user object in response
         */
        $user = self::authenticate_user( $username, $password );
        /**
         * Set the current user to the authenticated user
         */
        if ( empty( $user->data->ID ) ) {
            throw new UserError( __( 'The user could not be found', 'wp-graphql-jwt-authentication' ) );
        }
        /**
         * Set the current user as the authenticated user
         */
        wp_set_current_user( $user->data->ID );
        /**
         * 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 ),
            'user'         => DataSource::resolve_user( $user->data->ID ),
        ];
        /**
         * Let the user modify the data before send it back
         */
        return ! empty( $response ) ? $response : [];
    }
    /**
     * Get the issued time for the token
     *
     * @return int
     */
    public static function get_token_issued() {
        if ( ! isset( self::$issued ) ) {
            self::$issued = time();
        }
        return self::$issued;
    }
    /**
     * Returns the expiration for the token
     *
     * @return mixed|string|null
     */
    public static function get_token_expiration() {
        if ( ! isset( self::$expiration ) ) {
            /**
             * Set the expiration time, default is 300 seconds.
             */
            $expiration = self::get_token_issued() + 300;
            /**
             * Determine the expiration value. Default is 7 days, but is filterable to be configured as needed
             *
             * @param string $expiration The timestamp for when the token should expire
             */
            self::$expiration = apply_filters( 'graphql_jwt_auth_expire', $expiration );
        }
        return ! empty( self::$expiration ) ? self::$expiration : null;
    }
    /**
     * @param $user
     *
     * @return null|string
     */
    protected static function get_signed_token( \WP_User $user, $cap_check = true ) {
        /**
         * Only allow the currently signed in user access to a JWT token
         */
        if ( true === $cap_check && wp_get_current_user()->ID !== $user->ID || 0 === $user->ID ) {
            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' ) );
        }
        /**
         * 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
         */
        $not_before = apply_filters( 'graphql_jwt_auth_not_before', self::get_token_issued(), $user );
        /**
         * Configure the token array, which will be encoded
         */
        $token = [
            'iss'  => get_bloginfo( 'url' ),
            'iat'  => self::get_token_issued(),
            'nbf'  => $not_before,
            'exp'  => self::get_token_expiration(),
            'data' => [
                'user' => [
                    'id' => $user->data->ID,
                ],
            ],
        ];
        /**
         * Filter the token, allowing for individual systems to configure the token as needed
         *
         * @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 );
        /**
         * Encode the token
         */
        JWT::$leeway = 60;
        $token       = JWT::encode( $token, self::get_secret_key() );
        /**
         * Filter the token before returning it, allowing for individual systems to override what's returned.
         *
         * 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
         */
        $token = apply_filters( 'graphql_jwt_auth_signed_token', $token, $user->ID );
        /**
         * Return the token
         */
        return ! empty( $token ) ? $token : null;
    }
    /**
     * Given a User ID, returns the user's JWT secret
     *
     * @param int $user_id
     *
     * @return mixed|string
     */
    public static function get_user_jwt_secret( $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' ) );
        }
        /**
         * Filter the capability that is tied to editing/viewing user JWT Auth info
         *
         * @param     string 'edit_users'
         * @param int $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
         */
        $is_current_user = ( $user_id === wp_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' ) );
        }
        /**
         * Get the stored secret
         */
        $secret = get_user_meta( $user_id, 'graphql_jwt_auth_secret', true );
        /**
         * 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 );
        }
        /**
         * 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
         */
        return apply_filters( 'graphql_jwt_auth_user_secret', $secret, $user_id );
    }
    /**
     * Given a User ID, issue a new JWT Auth Secret
     *
     * @param int $user_id The ID of the user the secret is being issued for
     *
     * @return string $secret The JWT User secret for the user.
     */
    public static function issue_new_user_secret( $user_id ) {
        /**
         * Get the current user secret
         */
        $secret = null;
        /**
         * If the JWT Secret is not revoked for the user, generate a new one
         */
        if ( ! Auth::is_jwt_secret_revoked( $user_id ) ) {
            /**
             * Generate a new one and store it
             */
            $secret = uniqid( 'graphql_jwt_' );
            update_user_meta( $user_id, 'graphql_jwt_auth_secret', $secret );
        }
        return ! is_wp_error( $secret ) ? $secret : null;
    }
    /**
     * Given a User, returns whether their JWT secret has been revoked or not.
     *
     * @param int $user_id
     *
     * @return bool
     */
    public static function is_jwt_secret_revoked( $user_id ) {
        $revoked = (bool) get_user_meta( $user_id, 'graphql_jwt_auth_secret_revoked', true );
        return isset( $revoked ) && true === $revoked ? true : false;
    }
    /**
     * Public method for getting an Auth token for a given user
     *
     * @param \WP_USer $user The user to get the token for
     *
     * @return null|string
     */
    public static function get_token( $user, $cap_check = true ) {
        return self::get_signed_token( $user, $cap_check );
    }
    public static function get_refresh_token( $user, $cap_check = 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 ) ) {
                /**
                 * 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;
            }
            return $token;
        }, 10, 2 );
        return self::get_signed_token( $user, $cap_check );
    }
    public static function is_refresh_token() {
        return true === self::$is_refresh_token ? true : false;
    }
    /**
     * Takes a username and password and authenticates the user and returns the authenticated user object
     *
     * @param string $username The username for the user to login
     * @param string $password The password for the user to login
     *
     * @return null|\WP_Error|\WP_User
     */
    protected static function authenticate_user( $username, $password ) {
        /**
         * Try to authenticate the user with the passed credentials
         */
        $user = wp_authenticate( sanitize_user( $username ), trim( $password ) );
        /**
         * If the authentication fails return a error
         */
        if ( is_wp_error( $user ) ) {
            $error_code = ! empty( $user->get_error_code() ) ? $user->get_error_code() : 'invalid login';
            throw new UserError( esc_html( $error_code ) );
        }
        return ! empty( $user ) ? $user : null;
    }
    /**
     * This is our Middleware to try to authenticate the user according to the
     * token send.
     *
     * @param (int|bool) $user Logged User ID
     *
     * @return mixed|false|\WP_User
     */
    public static function filter_determine_current_user( $user ) {
        /**
         * Validate the token, which will check the Headers to see if Authentication headers were sent
         *
         * @since 0.0.1
         */
        $token = Auth::validate_token();
        /**
         * If no token was generated, return the existing value for the $user
         */
        if ( empty( $token ) ) {
            /**
             * Return the user that was passed in to the filter
             */
            return $user;
            /**
             * If there is a token
             */
        } else {
            /**
             * Get the current user from the token
             */
            $user = ! empty( $token ) && ! empty( $token->data->user->id ) ? $token->data->user->id : $user;
        }
        /**
         * Everything is ok, return the user ID stored in the token
         */
        return $user;
    }
    /**
     * Given a user ID, if the ID is for a valid user and the current user has proper capabilities, this revokes
     * the JWT Secret from the user.
     *
     * @param int $user_id
     *
     * @return mixed|boolean|\WP_Error
     */
    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 int $user_id
         */
        $capability = apply_filters( 'graphql_jwt_auth_edit_users_capability', 'edit_users', $user_id );
        /**
         * If the current user can edit users, or the current user is the user being edited
         */
        if (
            0 !== get_user_by( 'id', $user_id )->ID &&
            (
                current_user_can( $capability ) ||
                $user_id === wp_get_current_user()->ID
            )
        ) {
            /**
             * Set the user meta as true, marking the secret as revoked
             */
            update_user_meta( $user_id, 'graphql_jwt_auth_secret_revoked', 1 );
            return true;
        } else {
            return new \WP_Error( 'graphql-jwt-auth-cannot-revoke-secret', __( 'The JWT Auth Secret cannot be revoked for this user', 'wp-graphql-jwt-authentication' ) );
        }
    }
    /**
     * Given a user ID, if the ID is for a valid user and the current user has proper capabilities, this unrevokes
     * the JWT Secret from the user.
     *
     * @param int $user_id
     *
     * @return mixed|boolean|\WP_Error
     */
    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 int $user_id
         */
        $capability = apply_filters( 'graphql_jwt_auth_edit_users_capability', 'edit_users', $user_id );
        /**
         * If the user_id is a valid user, and the current user can edit_users
         */
        if ( 0 !== get_user_by( 'id', $user_id )->ID && current_user_can( $capability ) ) {
            /**
             * Issue a new user secret, invalidating any that may have previously been in place, and mark the
             * revoked meta key as false, showing that the secret has not been revoked
             */
            Auth::issue_new_user_secret( $user_id );
            update_user_meta( $user_id, 'graphql_jwt_auth_secret_revoked', 0 );
            return true;
        } else {
            return new \WP_Error( 'graphql-jwt-auth-cannot-unrevoke-secret', __( 'The JWT Auth Secret cannot be unrevoked for this user', 'wp-graphql-jwt-authentication' ) );
        }
    }
    protected static function set_status( $status_code ) {
        add_filter( 'graphql_response_status_code', function() use ( $status_code ) {
            return $status_code;
        });
    }
    /**
     * Main validation function, this function try to get the Authentication
     * headers and decoded.
     *
     * @param string $token The encoded JWT Token
     *
     * @throws \Exception
     * @return mixed|boolean|string
     */
    public static function validate_token( $token = null, $refresh = false ) {
        self::$is_refresh_token = ( true === $refresh ) ? true : false;
        /**
         * If a token isn't passed to the method, check the Authorization Headers to see if a token was
         * passed in the headers
         *
         * @since 0.0.1
         */
        if ( empty( $token ) ) {
            /**
             * Get the Auth header
             */
            $auth_header = self::get_auth_header();
            /**
             * If there's no $auth, return an error
             *
             * @since 0.0.1
             */
            if ( empty( $auth_header ) ) {
                return false;
            } else {
                /**
                 * The HTTP_AUTHORIZATION is present verify the format
                 * if the format is wrong return the user.
                 */
                list( $token ) = sscanf( $auth_header, 'Bearer %s' );
            }
        }
        /**
         * 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' ) );
        }
        /**
         * Try to decode the token
         */
        try {
            /**
             * Decode the Token
             */
            JWT::$leeway = 60;
            $secret = self::get_secret_key();
            $token = ! empty( $token ) ? JWT::decode( $token, $secret, [ 'HS256' ] ) : null;
            /**
             * The Token is decoded now validate the iss
             */
            if ( get_bloginfo( 'url' ) !== $token->iss ) {
                throw new \Exception( __( 'The iss do not match with this server', 'wp-graphql-jwt-authentication' ) );
            }
            /**
             * 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' ) );
            }
            /**
             * 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 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' ) );
        }
        self::$is_refresh_token = false;
        return $token;
    }
    /**
     * Get the value of the Authorization header from the $_SERVER super global
     *
     * @return mixed|string
     */
    public static function get_auth_header() {
        /**
         * Looking for the HTTP_AUTHORIZATION header, if not present just
         * return the user.
         */
        $auth_header = isset( $_SERVER['HTTP_AUTHORIZATION'] ) ? $_SERVER['HTTP_AUTHORIZATION'] : false;
        /**
         * Double check for different auth header string (server dependent)
         */
        $redirect_auth_header = isset( $_SERVER['REDIRECT_HTTP_AUTHORIZATION'] ) ? $_SERVER['REDIRECT_HTTP_AUTHORIZATION'] : false;
        /**
         * If the $auth header is set, use it. Otherwise attempt to use the $redirect_auth header
         */
        $auth_header = isset( $auth_header ) ? $auth_header : ( isset( $redirect_auth_header ) ? $redirect_auth_header : null );
        /**
         * Return the auth header, pass through a filter
         *
         * @param string $auth_header The header used to authenticate a user's HTTP request
         */
        return apply_filters( 'graphql_jwt_auth_get_auth_header', $auth_header );
    }
    public static function get_refresh_header() {
        /**
         * Check to see if the incoming request has a "Refresh-Authorization" header
         */
        $refresh_header = isset( $_SERVER['HTTP_REFRESH_AUTHORIZATION'] ) ? $_SERVER['HTTP_REFRESH_AUTHORIZATION'] : false;
        return apply_filters( 'graphql_jwt_auth_get_refresh_header', $refresh_header );
    }
}