grails / grails-spring-security-rest

Grails plugin to implement token-based, RESTful authentication using Spring Security
http://alvarosanchez.github.io/grails-spring-security-rest/
Apache License 2.0
202 stars 116 forks source link

OAuthException: Response body is incorrect. #327

Closed angelique360 closed 4 years ago

angelique360 commented 7 years ago

Hi, The plugin throws the following error when trying to sign in with facebook.

error:500, message:org.scribe.exceptions.OAuthException: Response body is incorrect. Can't extract a token from this: '{"access_token":"EAAOWKGC6MDcBAB9ZAka1zEc1","token_type":"bearer"}', error_description:org.scribe.exceptions.OAuthException: Response body is incorrect. Can't extract a token from this: '{"access_token":"EAAOWKGC6M","token_type":"bearer"}', error_code:OAuthException

atifsaddique211f commented 7 years ago

I am also getting the same issue for facebook login

angelique360 commented 7 years ago

Hello friend, I solve my problem by doing facebook login by hand, example:

   def code = params.code
   def query = "https://graph.facebook.com/v2.8/oauth/access_token?client_id=********&redirect_uri=*****&client_secret=****&code=${params.code}

 //call http
    def response = ApiClient.get(query)

    if(response?.code == HttpStatus.SC_OK){
        def access_token = JSON.parse(response.message)?.access_token

        def response2 = ApiClient.get("https://graph.facebook.com/v2.8/me?fields=id,name,first_name,middle_name,last_name,email,picture.height(200).width(200)&access_token="+access_token)

        if(response2?.code == HttpStatus.SC_OK){
            def profile =  JSON.parse(response2.message)

            def uid = profile?.id ?: null
            def first_name = profile?.first_name  ?: null
            def last_name = profile?.last_name  ?: null
            def email = profile?.email  ?: null
            def pictureUrl = profile?.picture  ? profile?.picture?.data?.url : null

            def userInstance = User.findByUsername(uid)
            def role = null

            if (!userInstance) {
               //Insert user

            } 

            springSecurityService.reauthenticate(userInstance.username)
           //use and import service userDetailsService
            def userDetail =  userDetailsService.loadUserByUsername(userInstance.username)
           //use and import service tokenGenerator
            def tokenValue = tokenGenerator.generateAccessToken(userDetail)

    //return or redirec
          return tokenValue
atifsaddique211f commented 7 years ago

@angelique360 Thanks a lot for sharing your solution. I am going to try it.

sergioricardoptech commented 7 years ago

I have the same problem...

package org.scribe.extractors;

import java.util.regex.*;

import org.scribe.exceptions.*;
import org.scribe.model.*;
import org.scribe.utils.*;

public class TokenExtractor20Impl implements AccessTokenExtractor
{
  private static final String TOKEN_REGEX = "access_token=([^&]+)";
  private static final String EMPTY_SECRET = "";
  /**
   * {@inheritDoc} 
   */
  public Token extract(String response)
  {
    Preconditions.checkEmptyString(response, "Response body is incorrect. Can't extract a token from an empty string");

    Matcher matcher = Pattern.compile(TOKEN_REGEX).matcher(response);
    if (matcher.find())
    {
      String token = OAuthEncoder.decode(matcher.group(1));
      return new Token(token, EMPTY_SECRET, response);
    } 
    else
    {
      throw new OAuthException("Response body is incorrect. Can't extract a token from this: '" + response + "'", null);
    }
  }
}

This class use TOKEN_REGEX for extract the token but now the respose is a Json.

https://developers.facebook.com/docs/apps/changelog

[Oauth Access Token] Format - The response format of https://www.facebook.com/v2.3/oauth/access_token returned when you exchange a code for an access_token now return valid JSON instead of being URL encoded. The new format of this response is {"access_token": {TOKEN}, "token_type":{TYPE}, "expires_in":{TIME}}. We made this update to be compliant with section 5.1 of RFC 6749.

Prakash-Thete commented 7 years ago

I had the same problem. So basically response from facebook is now coming as JSON.

The solution I have derived is to write our own custom facebook Api like below

Create the class FacebookCustomApi under /src/java/

public class FacebookCustomApi extends DefaultApi20{

    private static final String AUTHORIZE_URL           = "https://www.facebook.com/dialog/oauth?response_type=code&client_id=%s&redirect_uri=%s";
    private static final String SCOPED_AUTHORIZE_URL    = AUTHORIZE_URL + "&scope=%s";

    @Override
    public String getAccessTokenEndpoint() {
        return "https://graph.facebook.com/oauth/access_token";
    }

    @Override
    public AccessTokenExtractor getAccessTokenExtractor() {
        return new AccessTokenExtractor() {

            @Override
            public Token extract(String response) {

                Preconditions.checkEmptyString(response, "Response body is incorrect. Can't extract a token from an empty string");
                try {
                    //Here is the real deal, Just create the JSON from response and get the access token from it
                    JSONObject json = new JSONObject(response);
                    String token = json.getString("access_token");

                    return new Token(token, "", response);
                } catch (Exception e){
                    throw new OAuthException("Response body is incorrect. Can't extract a token from this: '" + response + "'", null);
                }
            }
        };
    }

    @Override
    public String getAuthorizationUrl(OAuthConfig config) {
        // Append scope if present
        if (config.hasScope()) {
            return String.format(SCOPED_AUTHORIZE_URL, config.getApiKey(),
                    OAuthEncoder.encode(config.getCallback()),
                    OAuthEncoder.encode(config.getScope()));
        } else {
            return String.format(AUTHORIZE_URL, config.getApiKey(),
                    OAuthEncoder.encode(config.getCallback()));
        }
    }

    @Override
    public Verb getAccessTokenVerb() {
        return Verb.POST;
    }

    @Override
    public OAuthService createService(OAuthConfig config) {
        return new FacebookOAuth2Service(this, config);
    }

    private class FacebookOAuth2Service extends OAuth20ServiceImpl {

        private static final String GRANT_TYPE_AUTHORIZATION_CODE   = "authorization_code";
        private static final String GRANT_TYPE                      = "grant_type";
        private DefaultApi20 api;
        private OAuthConfig config;

        public FacebookOAuth2Service(DefaultApi20 api, OAuthConfig config) {
            super(api, config);
            this.api    = api;
            this.config = config;
        }

        @Override
        public Token getAccessToken(Token requestToken, Verifier verifier) {
            OAuthRequest request = new OAuthRequest(api.getAccessTokenVerb(), api.getAccessTokenEndpoint());
            switch (api.getAccessTokenVerb()) {
                case POST:
                    request.addBodyParameter(OAuthConstants.CLIENT_ID, config.getApiKey());
                    request.addBodyParameter(OAuthConstants.CLIENT_SECRET, config.getApiSecret());
                    request.addBodyParameter(OAuthConstants.CODE, verifier.getValue());
                    request.addBodyParameter(OAuthConstants.REDIRECT_URI, config.getCallback());
                    request.addBodyParameter(GRANT_TYPE, GRANT_TYPE_AUTHORIZATION_CODE);
                    break;
                case GET:
                default:
                    request.addQuerystringParameter(OAuthConstants.CLIENT_ID, config.getApiKey());
                    request.addQuerystringParameter(OAuthConstants.CLIENT_SECRET, config.getApiSecret());
                    request.addQuerystringParameter(OAuthConstants.CODE, verifier.getValue());
                    request.addQuerystringParameter(OAuthConstants.REDIRECT_URI, config.getCallback());
                    if (config.hasScope()) request.addQuerystringParameter(OAuthConstants.SCOPE, config.getScope());
            }
            Response response = request.send();
            return api.getAccessTokenExtractor().extract(response.getBody());
        }
    }
}

And use it in our oauth config as below

//for facebook authentication

oauth {
    providers {
        facebook {
            api = FacebookCustomApi
hamza3202 commented 7 years ago

@Prakash-Thete Your solution doesn't seem to be working for me, i am unable to over-ride the API? any guesses why?

ivarprudnikov commented 7 years ago

Instead of overriding whole of FacebookApi class I searched for the way to replace AccessTokenExtractor getAccessTokenExtractor() method which by default uses org.scribe.extractors.TokenExtractor20Impl, I wanted to replace it with org.scribe.extractors.JsonTokenExtractor

It was a bit odd as class that needs to have method overridden ExtendedFacebookApi is declared final so had to rewrite it:

OverridenFacebookApi.groovy

package foobar

import org.scribe.builder.api.StateApi20
import org.scribe.extractors.AccessTokenExtractor
import org.scribe.extractors.JsonTokenExtractor
import org.scribe.model.OAuthConfig
import org.scribe.utils.OAuthEncoder
import org.scribe.utils.Preconditions

class OverridenFacebookApi extends StateApi20 {

    private static final String AUTHORIZE_URL_WITH_STATE = "https://www.facebook.com/dialog/oauth?display=popup&client_id=%s&redirect_uri=%s&state=%s";
    private static final String SCOPED_AUTHORIZE_URL_WITH_STATE = AUTHORIZE_URL_WITH_STATE + "&scope=%s";

    @Override
    AccessTokenExtractor getAccessTokenExtractor() {
        return new JsonTokenExtractor()
    }

    @Override
    String getAccessTokenEndpoint() {
        return "https://graph.facebook.com/oauth/access_token";
    }

    @Override
    String getAuthorizationUrl(final OAuthConfig config,
                                      final String state) {
        Preconditions
                .checkEmptyString(config.getCallback(),
                "Must provide a valid url as callback. Facebook does not support OOB");

        // Append scope if present
        if (config.hasScope()) {
            return String.format(SCOPED_AUTHORIZE_URL_WITH_STATE,
                    config.getApiKey(),
                    OAuthEncoder.encode(config.getCallback()),
                    OAuthEncoder.encode(state),
                    OAuthEncoder.encode(config.getScope()));
        } else {
            return String.format(AUTHORIZE_URL_WITH_STATE, config.getApiKey(),
                    OAuthEncoder.encode(config.getCallback()),
                    OAuthEncoder.encode(state));
        }
    }

}

Then had to make sure FacebookClient is using it, so I've extended it and changed initialisation to use the class above.

OverridenFacebookClient.groovy

package foobar

import groovy.transform.InheritConstructors
import org.apache.commons.lang3.StringUtils
import org.pac4j.core.util.CommonHelper
import org.pac4j.oauth.client.FacebookClient
import org.scribe.model.OAuthConfig
import org.scribe.model.SignatureType
import org.scribe.oauth.StateOAuth20ServiceImpl

@InheritConstructors
class OverridenFacebookClient extends FacebookClient {

    @Override
    protected void internalInit() {
        super.internalInit();
        CommonHelper.assertNotBlank("fields", this.fields);
        this.api20 = new OverridenFacebookApi();
        if(StringUtils.isNotBlank(this.scope)) {
            this.service = new StateOAuth20ServiceImpl(this.api20, new OAuthConfig(this.key, this.secret, this.callbackUrl, SignatureType.Header, this.scope, (OutputStream)null), this.connectTimeout, this.readTimeout, this.proxyHost, this.proxyPort);
        } else {
            this.service = new StateOAuth20ServiceImpl(this.api20, new OAuthConfig(this.key, this.secret, this.callbackUrl, SignatureType.Header, (String)null, (OutputStream)null), this.connectTimeout, this.readTimeout, this.proxyHost, this.proxyPort);
        }

    }
}

Finally had to reference it in Config.groovy

import foobar.OverridenFacebookClient

grails.plugin.springsecurity.rest.oauth.facebook.client = OverridenFacebookClient
pabloalba commented 7 years ago

Awesome workaround @ivarprudnikov !

Just a fix. On OverridenFacebookClient.internalInit you miss the context param. It should be:

    @Override
    protected void internalInit(final WebContext context) {
        ...
    }
alvarosanchez commented 4 years ago

After upgrading pac4j (#416), the Facebook API supported now is 2.11