4

I want to do server-to-server authentication for my API using OAuth2 and Symfony2 (Symfony3 actually). I am using FOSOAuthServerBundle.

The remote server will not be making requests on behalf of any user, so I believe client_credentials is the proper grant type to use.

I have created a client and am able to obtain an access token. However, I am having an issue with protecting an endpoint with a client_credentials token. This is what is in my security.yml

firewalls:
    api:
        pattern:    ^/api/v1
        fos_oauth:  true
        stateless:  true
        anonymous:  false # can be omitted as its default value

access_control:
    - { path: ^/api/v1, roles: [ IS_AUTHENTICATED_FULLY ] }

When I try to access something in /api/v1 using the Bearer Authorization header, I get an error

Full authentication is required to access this resource

Here is a sample request:

GET /app_dev.php/api/v1/user HTTP/1.1
Host: local.dev
Authorization: Bearer NDFhMzViZjQ2YjMyYjFlNzBjZTZiMTU2ZjdhY2I4ZmZhZjY2MmVkMjU3NzNjNDE2NGI2YzEzMWFjZGQ5MzE4NA

I assume the problem is that since I am using client_credentials there is no user and thus IS_FULLY_AUTHENTICATED is not true. If that is the case, how can I authenticate the client without a user?

Note: I have also tried removing the lines

access_control:
    - { path: ^/api/v1, roles: [ IS_AUTHENTICATED_FULLY ] }

then trying to access the client details in the controller with the following:

$tokenManager = $this->get('fos_oauth_server.access_token_manager.default');
$accessToken = $tokenManager->findTokenByToken(
    $this->get('security.token_storage')->getToken()->getToken()
);
$client = $accessToken->getClient();

but then I get the error:

Attempted to call an undefined method named "getToken" of class "Symfony\Component\Security\Core\Authentication\Token\AnonymousToken"

2 Answers 2

2

I spend a day struggling with the same situation (also Symfony 3). In the end the solution was quite simple for me.

In my security.yml I defined the following:

firewalls:
    # disables authentication for assets and the profiler, adapt it according to your needs
    dev:
        pattern: ^/(_(profiler|wdt)|css|images|js)/
        security: false

    main:
        anonymous: ~
        # activate different ways to authenticate

        # http_basic: ~
        # http://symfony.com/doc/current/book/security.html#a-configuring-how-your-users-will-authenticate

        # form_login: ~
        # http://symfony.com/doc/current/cookbook/security/form_login_setup.html

    oauth_token:
        pattern:    ^/oauth/v2/token
        security:   false

    oauth_authorize:
        pattern:    ^/oauth/v2/auth
        # Add your favorite authentication process here
        form_login:
            provider: userprovider
            check_path: /oauth/v2/auth_login_check
            login_path: /oauth/v2/auth_login
        #anonymous: true #allow all requests

    api:
        pattern:    ^/api
        fos_oauth:  true
        stateless:  true
        anonymous:  false

access_control:
    - { path: ^/oauth/v2/token, roles: [ IS_AUTHENTICATED_ANONYMOUSLY, IS_AUTHENTICATED_FULLY ] }
    - { path: ^/api, roles: [ IS_AUTHENTICATED_FULLY ] }

There was a little error in there. I left the main in the firewall open for all urls:

  main:
        anonymous: ~
        # activate different ways to authenticate

By removing the main and its childeren or defining a proper "pattern" which does not override the pattern of the api firewall, my OAuth2 server was working correctly.

Later I found out that the order of the firewalls is very important. Simply defining the API firewall rules before the main(default) firewall rules works like a charm. So the following is correct:

firewalls:
    # disables authentication for assets and the profiler, adapt it according to your needs
    dev:
        pattern: ^/(_(profiler|wdt)|css|images|js)/
        security: false



    oauth_token:
        pattern:    ^/oauth/v2/token
        security:   false

    oauth_authorize:
        pattern:    ^/oauth/v2/auth
        # Add your favorite authentication process here
        form_login:
            provider: userprovider
            check_path: /oauth/v2/auth_login_check
            login_path: /oauth/v2/auth_login
        #anonymous: true #allow all requests

    api:
        pattern:    ^/api
        fos_oauth:  true
        stateless:  true
        anonymous:  false

    main:
        anonymous: ~
        # activate different ways to authenticate

        # http_basic: ~
        # http://symfony.com/doc/current/book/security.html#a-configuring-how-your-users-will-authenticate

        # form_login: ~
        # http://symfony.com/doc/current/cookbook/security/form_login_setup.html

access_control:
    - { path: ^/oauth/v2/token, roles: [ IS_AUTHENTICATED_ANONYMOUSLY, IS_AUTHENTICATED_FULLY ] }
    - { path: ^/api, roles: [ IS_AUTHENTICATED_FULLY ] }
Sign up to request clarification or add additional context in comments.

Comments

1

I figured out a way to authenticate inside the controller but I am not sure if this is the best way to accomplish what I want. It seems like FOSOAuthServerBundle should be able to do all these checks for me.

<?php

namespace ApiBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Exception\TokenNotFoundException;
use Symfony\Component\Security\Core\Exception\AuthenticationExpiredException;
use FOS\OAuthServerBundle\Security\Authentication\Token\OAuthToken;
use Symfony\Component\HttpFoundation\JsonResponse;

class UserApiController extends Controller
{
    /**
     * @Route("/user", name="user")
     */
    public function indexAction(Request $request)
    {
        $authenticationErrorResponse = $this->checkAuthAndGetErrorResponse($request);
        if ($authenticationErrorResponse) {
            return $authenticationErrorResponse;
        }

        // all good, now do something
    }

    private function checkAuthAndGetErrorResponse(Request $request)
    {
        $tokenManager = $this->get('fos_oauth_server.access_token_manager.default');

        $bearerToken = $this->get('fos_oauth_server.server')->getBearerToken($request);
        if (!$bearerToken) {
            return new JsonResponse(['status' => 400, 'message' => 'Bearer token not supplied'], 400);
        }

        $accessToken = $tokenManager->findTokenByToken($bearerToken);

        if (!$accessToken) {
            return new JsonResponse(['status' => 400, 'message' => 'Bearer token not valid'], 400);
        }

        if ($accessToken->hasExpired()) {
            return new JsonResponse(['status' => 400, 'message' => 'Access token has expired'], 400);
        }

        // may want to validate something else about the client, but that is beyond OAuth2 scope
        //$client = $accessToken->getClient();

        return null;
    }
}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.