Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
167 views
in Technique[技术] by (71.8m points)

php - How do we implement custom API-only authentication in Laravel

This isn't a question so much in need of an answer, but further suggestions and answers and recommendations are welcome. I want to share with the world how I resolved this issue and hope it helps others.

Laravel comes with several pre-designed authentication solutions that you can spin up with a few artisan commands. These include:

  • standard users table authentication
  • OAuth2 (via the Laravel Passport package)
  • Social media based authentication (via the Laravel Socialite package)

As useful as all of these are, in this age of micro-services, Laravel doesn't provide much in the way of an out-of-the-box bootstrap for API-only authentication using custom APIs.

I was faced with this problem several months ago and I searched Google and Stackoverflow for an answer. I found helpful articles which helped to point the way, and these are cited. It took some effort to understand how to glue them together and step-debugging to iron out the kinks.

The answer is provided in the hope that it helps others - and myself, where I have to do the same thing again in the future.

Assumptions and Scope:

  • you've created your own API like https://example.com/login and https://example.com/logout
  • you're running a website that requires authentication, but not via models and tables or social media
  • your API manages interactions with tables, including user-login/logout
  • you use the Laravel Passport add-on for OAuth2 authentication (acknowledgements to @ShuvoJoseph for bringing this to my attention)
See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Answer

0 votes
by (71.8m points)

The solution involves seven PHP files

  • app/Http/Controllers/HomeController.php - homepage controller; the destination for an authenticated user
  • app/Providers/ApiUserProvider.php - a custom provider to bootstrap and register the logged-in user, and implements the interface IlluminateContractsAuthUserProvider
  • app/CoreExtensions/SessionGuardExtended.php - custom guard-controller to log-in the user and receives the authentication values and stores them in session array; extends class IlluminateAuthSessionGuard
  • app/ApiUser - if you're using OAuth2 (Laravel's Passport); custom user class that exposes the OAuth access_token; extends IlluminateAuthGenericUser and implements the interface IlluminateContractsAuthAuthenticatable
  • config/auth.php - the auth config which instructs the Auth() facade to return the custom session guard
  • app/Providers/AuthServiceProvider.php - the auth bootstrap
  • app/Providers/AppServiceProvider.php - the main application bootstrap

Source research/investigation material are cited for you to investigate for yourself and comprehend the background context to their existence. I make no claims to be a genius who created the solution from scratch through my own mojo, but rather that - like all innovators - I build on the efforts of others. The unique selling point of my article is that I provide a complete packaged solution, whereas the cited sources provide solutions to niche parts of the overall answer. Together, after much trial and error, they helped me to form a complete solution.

A really useful article to understands how config/auth.php affects execution in AuthManager.php is https://www.2hatslogic.com/blog/laravel-custom-authentication/

No code modifications are made to the following, but they're included to acknowledge the role they play and their importance in the process:

  • vendor/laravel/framework/src/Illuminate/Auth/AuthManager.php - main authorization factory manager
  • Auth() facade - returns the shrink-wrapped IlluminateAuthSessionGuard class instance by default, unless it's instructed to do otherwise through the config/auth.php file - Auth() is used ubiquitously throughout Laravel code to retrieve the session guard

The Code

app/Http/Controllers/HomeController.php

<?php
namespace AppHttpControllers;

use IlluminateHttpRequest;

/**
 * Handles and manages the home-page
 * 
 * @category controllers
 */
class HomeController extends Controller
{
    /**
     * Create a new controller instance.
     *
     * @return void
     */
    public function __construct()
    {
        $this->middleware('auth');
    }

    public function index()
    {
        blah
    }

    ... other methods ... 

}

app/Providers/ApiUserProvider.php

Sources:

<?php
namespace AppProviders;

use IlluminateContractsAuthUserProvider;
use IlluminateContractsAuthAuthenticatable as UserContract;
use AppApiUser;

/**
 * Delegates API user login and authentication
 * 
 * @category providers
 */
class ApiUserProvider implements UserProvider
{
    
    /**
     * Custom API Handler 
     * Used to request API and capture responses
     * 
     * @var PathToYourInternalApiHandler
     */
    private $_oApi = null;
    
    /**
     * POST request to API
     * 
     * @param string  $p_url      Endpoint URL
     * @param array   $p_arrParam Parameters
     * @param boolean $p_isOAuth2 Is OAuth2 authenticated request? [Optional, Default=True]
     * 
     * @return array
     */
    private function _post(string $p_url, array $p_arrParam, bool $p_isOAuth2=true)
    {
        if (!$this->_oApi) {
            $this->_oApi = new PathToYourInternalApiHandler();
        }
        $arrResponse = $this->_oApi->post($p_url, $p_arrParam, $p_isOAuth2);
        return $arrResponse;
    }
    
    /**
     * GET request to API
     * 
     * @param string $p_url     Endpoint URL
     * @param array $p_arrParam Parameters [Optional, Default = array()]
     * 
     * @return array
     */
    private function _get(string $p_url, array $p_arrParam=[], bool $p_isOAuth2=true)
    {   
        if (!$this->_oApi) {
            $this->_oApi = new PathToYourInternalApiHandler();
        }
        $arrResponse = $this->_oApi->get($p_url, $p_arrParam);
        return $arrResponse;
    }
    
    /**
     * Retrieve a user by the given credentials.
     *
     * @param array $p_arrCredentials
     * 
     * @return IlluminateContractsAuthAuthenticatable|null
     */
    public function retrieveByCredentials(array $p_arrCredentials)
    {
        $arrResponse = $this->_post('/login', $p_arrCredentials, false);
        if ( $arrResponse['result'] ) {
            $arrPayload = array_merge(
                $arrResponse['data'],
                $p_arrCredentials
            );
            return $this->getApiUser($arrPayload);
        }
    }

    /**
     * Retrieve a user by their unique identifier.
     *
     * @param mixed $p_id
     * 
     * @return IlluminateContractsAuthAuthenticatable|null
     */
    public function retrieveById($p_id)
    {
        $arrResponse = $this->_get("user/id/{$p_id}");        
        if ( $arrResponse['result'] ) {
            return $this->getApiUser($arrResponse['data']);
        }
    }

    /**
     * Validate a user against the given credentials.
     *
     * @param IlluminateContractsAuthAuthenticatable $p_oUser
     * @param array                                      $p_arrCredentials
     * 
     * @return bool
     */
    public function validateCredentials(UserContract $p_oUser, array $p_arrCredentials)
    {
        return $p_oUser->getAuthPassword() == $p_arrCredentials['password'];
    }

    /**
     * Get the api user.
     *
     * @param mixed $p_user
     * 
     * @return AppAuthApiUser|null
     */
    protected function getApiUser($p_user)
    {
        if ($p_user !== null) {
            return new ApiUser($p_user);
        }
        return null;
    }

    protected function getUserById($id)
    {
        $user = [];

        foreach ($this->getUsers() as $item) {
            if ($item['account_id'] == $id) {
                $user = $item;

                break;
            }
        }

        return $user ?: null;
    }

    protected function getUserByUsername($username)
    {
        $user = [];

        foreach ($this->getUsers() as $item) {
            if ($item['email_address'] == $username) {
                $user = $item;

                break;
            }
        }

        return $user ?: null;
    }
    

    /**
     * The methods below need to be defined because of the Authenticatable contract
     * but need no implementation for 'Auth::attempt' to work and can be implemented
     * if you need their functionality
     */
    public function retrieveByToken($identifier, $token) { }
    public function updateRememberToken(UserContract $user, $token) { }
    
}

app/CoreExtensions/SessionGuardExtended.php

Sources:

<?php
namespace AppCoreExtensions;

use IlluminateAuthSessionGuard;
use IlluminateContractsAuthAuthenticatable;

/**
 * Extended SessionGuard() functionality 
 * Provides added functionality to store the OAuth tokens in the session for later use
 * 
 * @category guards
 * 
 * @see https://stackoverflow.com/questions/36087061/extending-laravel-5-2-sessionguard
 */
class SessionGuardExtended extends SessionGuard
{
    
    /**
     * Log a user into the application.
     *
     * @param  IlluminateContractsAuthAuthenticatable  $p_oUser
     * @param  bool  $p_remember
     * @return void
     */
    public function login(Authenticatable $p_oUser, $p_remember = false)
    {
        
        parent::login($p_oUser, $p_remember);
        
        /**
         * Writing the OAuth tokens to the session
         */
        $key = 'authtokens';
        $this->session->put(
            $key, 
            [
                'access_token' => $p_oUser->getAccessToken(),
                'refresh_token' => $p_oUser->getRefreshToken(),
            ]
        );
    }
    
    /**
     * Log the user out of the application.
     *
     * @return void
     */
    public function logout()
    {
        parent::logout();
        
        /**
         * Deleting the OAuth tokens from the session
         */
        $this->session->forget('authtokens');        
    }
    
}

app/ApiUser

Sources:

<?php
namespace App;

use IlluminateAuthGenericUser;
use IlluminateContractsAuthAuthenticatable as UserContract;

class ApiUser extends GenericUser implements UserContract
{
    
    /**
     * Returns the OAuth access_token
     * 
     * @return mixed
     */
    public function getAccessToken()
    {
        return $this->attributes['access_token'];
    }
    
    
    public function getRefreshToken()
    {
        return $this->attributes['refresh_token'];
    }
    
}

app/Providers/AuthServiceProvider.php

<?php
namespace AppProviders;

use IlluminateSupportFacadesAuth;
use IlluminateFoundationSupportProvidersAuthServiceProvider as ServiceProvider;

class AuthServiceProvider extends ServiceProvider
{
    
    /**
     * Register any authentication / authorization services.
     *
     * @return void
     */
    public function boot()
    {
        $this->registerPolicies();
        
        Auth::provider('frank_sinatra', function ($app, array $config) {
            // Return an instance of IlluminateContractsAuthUserProvider...

        

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...