質問

  • I have a web application that uses omniauth-google-oauth2 for authenticating using a Google account. The session is then kept using a cookie (session[:user_id]). This works.
  • I also have an Android application that uses the website services using REST/JSON API. The application uses AccountManager to get an access token. This also works.

I'm trying to get the Android application to make a request that makes the web application verify that token and start a session with the Android application (the cookies are persistent on the Android application).

I noticed that the callback URL from Google is: http://my.domain.com/auth/google_oauth2/callback, so I tried adding the parameters state="/" and code="<TOKEN FROM AccountManager>".

This caused:

(google_oauth2) Callback phase initiated.
(google_oauth2) Authentication failure! invalid_credentials: OmniAuth::Strategies::OAuth2::CallbackError, OmniAuth::Strategies::OAuth2::CallbackError

Started GET "/auth/google_oauth2/callback?state=%2F&code=<TOKEN FROM AccountManager>" for 5.102.217.111 at 2013-08-10 18:38:58 +0300

OmniAuth::Strategies::OAuth2::CallbackError (OmniAuth::Strategies::OAuth2::CallbackError):
  omniauth-oauth2 (1.1.1) lib/omniauth/strategies/oauth2.rb:71:in `callback_phase'
  omniauth (1.1.4) lib/omniauth/strategy.rb:226:in `callback_call'
  omniauth (1.1.4) lib/omniauth/strategy.rb:182:in `call!'
  omniauth (1.1.4) lib/omniauth/strategy.rb:164:in `call'
  omniauth (1.1.4) lib/omniauth/builder.rb:49:in `call'
  actionpack (3.2.13) lib/action_dispatch/middleware/best_standards_support.rb:17:in `call'
  rack (1.4.5) lib/rack/etag.rb:23:in `call'
  rack (1.4.5) lib/rack/conditionalget.rb:25:in `call'
  actionpack (3.2.13) lib/action_dispatch/middleware/head.rb:14:in `call'
  actionpack (3.2.13) lib/action_dispatch/middleware/params_parser.rb:21:in `call'
  actionpack (3.2.13) lib/action_dispatch/middleware/flash.rb:242:in `call'
  rack (1.4.5) lib/rack/session/abstract/id.rb:210:in `context'
  rack (1.4.5) lib/rack/session/abstract/id.rb:205:in `call'
  actionpack (3.2.13) lib/action_dispatch/middleware/cookies.rb:341:in `call'
  activerecord (3.2.13) lib/active_record/query_cache.rb:64:in `call'
  activerecord (3.2.13) lib/active_record/connection_adapters/abstract/connection_pool.rb:479:in `call'
  actionpack (3.2.13) lib/action_dispatch/middleware/callbacks.rb:28:in `block in call'
  activesupport (3.2.13) lib/active_support/callbacks.rb:405:in `_run__1566251561940761300__call__2926332968477140393__callbacks'
  activesupport (3.2.13) lib/active_support/callbacks.rb:405:in `__run_callback'
  activesupport (3.2.13) lib/active_support/callbacks.rb:385:in `_run_call_callbacks'
  activesupport (3.2.13) lib/active_support/callbacks.rb:81:in `run_callbacks'
  actionpack (3.2.13) lib/action_dispatch/middleware/callbacks.rb:27:in `call'
  actionpack (3.2.13) lib/action_dispatch/middleware/reloader.rb:65:in `call'
  actionpack (3.2.13) lib/action_dispatch/middleware/remote_ip.rb:31:in `call'
  actionpack (3.2.13) lib/action_dispatch/middleware/debug_exceptions.rb:16:in `call'
  actionpack (3.2.13) lib/action_dispatch/middleware/show_exceptions.rb:56:in `call'
  railties (3.2.13) lib/rails/rack/logger.rb:32:in `call_app'
  railties (3.2.13) lib/rails/rack/logger.rb:16:in `block in call'
  activesupport (3.2.13) lib/active_support/tagged_logging.rb:22:in `tagged'
  railties (3.2.13) lib/rails/rack/logger.rb:16:in `call'
  actionpack (3.2.13) lib/action_dispatch/middleware/request_id.rb:22:in `call'
  rack (1.4.5) lib/rack/methodoverride.rb:21:in `call'
  rack (1.4.5) lib/rack/runtime.rb:17:in `call'
  activesupport (3.2.13) lib/active_support/cache/strategy/local_cache.rb:72:in `call'
  rack (1.4.5) lib/rack/lock.rb:15:in `call'
  actionpack (3.2.13) lib/action_dispatch/middleware/static.rb:63:in `call'
  railties (3.2.13) lib/rails/engine.rb:479:in `call'
  railties (3.2.13) lib/rails/application.rb:223:in `call'
  rack (1.4.5) lib/rack/content_length.rb:14:in `call'
  railties (3.2.13) lib/rails/rack/log_tailer.rb:17:in `call'
  rack (1.4.5) lib/rack/handler/webrick.rb:59:in `service'
  /home/oded/.rvm/rubies/ruby-2.0.0-p195/lib/ruby/2.0.0/webrick/httpserver.rb:138:in `service'
  /home/oded/.rvm/rubies/ruby-2.0.0-p195/lib/ruby/2.0.0/webrick/httpserver.rb:94:in `run'
  /home/oded/.rvm/rubies/ruby-2.0.0-p195/lib/ruby/2.0.0/webrick/server.rb:295:in `block in start_thread'


  Rendered /home/oded/.rvm/gems/ruby-2.0.0-p195/gems/actionpack-3.2.13/lib/action_dispatch/middleware/templates/rescues/_trace.erb (0.8ms)
  Rendered /home/oded/.rvm/gems/ruby-2.0.0-p195/gems/actionpack-3.2.13/lib/action_dispatch/middleware/templates/rescues/_request_and_response.erb (0.7ms)
  Rendered /home/oded/.rvm/gems/ruby-2.0.0-p195/gems/actionpack-3.2.13/lib/action_dispatch/middleware/templates/rescues/diagnostics.erb within rescues/layout (5.3ms)

Both the web and Android applications are authorized in Google APIs Console.

I don't want to use GoogleAuthUtil for 3 reasons: I already implemented AccountManager completely, it at least doubles my application size, and I don't see how it solves the problem with my web app.

As a temporary solution I made a WebView activity that goes through the same process as the web application. That's not a good solution because the user needs to enter his email and password and he has no way to tell if it's phishing.

Gemfile:

gem 'omniauth-google-oauth2'

SessionsControler (app/controllers/sessions_controller.rb):

def new
  redirect_to "/auth/google_oauth2"
end

def create
  auth = request.env["omniauth.auth"]
  account = case auth['provider']
    when GoogleAccount::PROVIDER then GoogleAccount.find_by_omniauth(auth)
  end
  session[:user_id] = account.user.id
  respond_to do |format|
    format.html { redirect_to root_url, :notice => "Signed in!" }
    format.json { render json: { result: true } }
  end
end

GoogleAccount (app/models/google_account.rb):

after_validation :create_user!, on: :create

def self.find_by_omniauth(auth)
  google_account = find_by_uid(auth['uid']) || new(uid: auth['uid'])
  google_account.attributes = auth['info'].select { |k, v| k.in?(attribute_names) }
  google_account.save!
  google_account
end

SessionManager.java:

public void login() {
    _accountManager = AccountManager.get(_activity);
    final Account[] accounts = _accountManager.getAccountsByType("com.google");
    String[] accountNames = new String[accounts.length];
    for (int i = 0; i < accounts.length; i++) {
        accountNames[i] = accounts[i].name;
    }

    new AlertDialog.Builder(_activity).
    setItems(accountNames, new DialogInterface.OnClickListener() {
        @Override
        public void onClick(DialogInterface dialog, int which) {
            _account = accounts[which];
            getAuthToken();
        }
    }).
    show();
}

private void getAuthToken() {
    _accountManager.getAuthToken(
            _account, "ah", null, _activity,
            new AccountManagerCallback<Bundle>() {
                @Override
                public void run(AccountManagerFuture<Bundle> result) {
                    Bundle bundle;
                    try {
                        bundle = result.getResult();
                    } catch (OperationCanceledException e) {
                        // TODO: handle errors
                        e.printStackTrace();
                        return;
                    } catch (AuthenticatorException e) {
                        // TODO: handle errors
                        e.printStackTrace();
                        return;
                    } catch (IOException e) {
                        // TODO: handle errors
                        e.printStackTrace();
                        return;
                    }

                    Intent launch = (Intent) bundle.get(AccountManager.KEY_INTENT);
                    if (launch != null) {
                        _activity.startActivityForResult(launch, _accountManagerRequestCode);
                        return;
                    }

                    onGetAuthToken(bundle);
                }
            },
            null
            );
}

public void onAccountManagerResult(int resultCode, Intent data) {
    switch (resultCode) {
    case Activity.RESULT_OK:
        _accountManager = AccountManager.get(_activity);
        getAuthToken();
        break;
    }
}

private void onGetAuthToken(Bundle bundle) {
    final String token = bundle.getString(AccountManager.KEY_AUTHTOKEN);
    login(
            token,
            new LoginResponseHandler() {
                @Override
                public void onResult(boolean result, boolean hadAnonymous, String message, Map<String, List<String>> errors) {
                    if (result) {
                        setLoggedIn(true);
                    } else {
                        // Token expired?
                        _accountManager.invalidateAuthToken("com.google", token);
                        getAuthToken();
                    }
                }
            });
}

public static void login(String token, LoginResponseHandler loginResponseHandler) {
    RequestParams requestParams = defaultRequestParams();
    requestParams.put("code", token);
    requestParams.put("state", "/");
    _client.get(
            BASE_URL + "/auth/google_oauth2/callback", requestParams,
            new JsonLoginResponseHandler(loginResponseHandler));
}
役に立ちましたか?

解決

I managed to validate the token on the server side manually, without using omniauth.

  • I had to change the call to AccountManager#getAuthToken. The authTokenType parameter (previously "ah") needed to be filled with the client ID instead as follows: "audience:server:client_id:" + CLIENT_ID. The client ID is retrieved from the API Console, and is the one belongs to your web application. It looks like this: 123456789.apps.googleusercontent.com.
  • The code on the server side was pretty simple, so I didn't mind:

SessionsController (app/controllers/sessions_controller.rb):

def create
  auth = request.env["omniauth.auth"]
  if auth
    account = case auth['provider']
                when GoogleAccount::OAUTH_PROVIDER then GoogleAccount.find_by_omniauth(auth)
              end
  else
    account = case params[:provider]
                when GoogleAccount::PROVIDER then GoogleAccount.find_by_token(params[:token])
              end
  end
  render text: "Unauthoirized", status: :unauthorized and return if account.nil?
  session[:user_id] = account.user.id
    respond_to do |format|
    format.html { redirect_to root_url, :notice => "Signed in!" }
    format.json { render json: { result: true } }
  end
end

GoogleAccount (app/models/google_account.rb):

def self.find_by_token(token)
  validator = GoogleIDToken::Validator.new
  jwt = validator.check(token, CLIENT_ID)
  return if jwt.nil?

  google_account = where(uid: jwt['id']).first_or_initialize
  google_account.email = jwt['email']
  google_account.save!
  google_account
end

jwt is a Hash that looks like this:

{"iss"=>"accounts.google.com",
 "verified_email"=>"true",
 "email_verified"=>"true",
 "email"=>"my.email@gmail.com",
 "aud"=>"123456789.apps.googleusercontent.com", # aka CLIENT_ID
 "cid"=>"123456789-somerandomletters.apps.googleusercontent.com", # this is the device ID, you can validate that too with another parameter
 "azp"=>"123456789-samerandomletters.apps.googleusercontent.com",
 "id"=>"10000000000000000000", # this is Google's user ID for this user, keep it
 "sub"=>"10000000000000000000",
 "iat"=>1376306389,
 "exp"=>1376310289}

The server now doesn't get the same details it gets when logging in on the web as the token only holds the email, but that's enough for me. I also don't validate the CID (device ID), not sure when you'll want this.

他のヒント

With Google Play Services you can call GoogleAuthUtil.getToken to get the token in the Android app. See http://developer.android.com/google/play-services/auth.html.

I use rest-client to manually validate the token on the server. I based the headers on the Twitter OAuth Echo that I have used in the past.

require 'rest_client'
require 'json'

class ApplicationController < ActionController::Base
  # Prevent CSRF attacks by raising an exception.
  # For APIs, you may want to use :null_session instead.
  protect_from_forgery with: :exception

  before_filter :require_login_or_oauth!

  private

  def authorization_header
    'X-Verify-Credentials-Authorization'
  end

  def service_provider_header
    'X-Auth-Service-Provider'
  end

  def verify_url
    'https://www.googleapis.com/oauth2/v1/tokeninfo'
  end

  def client_ids
      %w(-- your client ids here--)
  end

  def find_oauth_user
    if request.headers[authorization_header].nil? or verify_url != request.headers[service_provider_header]
      logger.warn "bad authorization headers"
      render :text => "Bad authorization headers", :status => :unauthorized
      return nil
    end
    begin
      response = RestClient.get verify_url, { :params => { :access_token => request.headers[authorization_header] }, :accept => :json }
      if 200 != response.code
        render :text => "OAuth validation failed", :status => :unauthorized
        return nil
      end
      body = JSON.parse(response.to_str)

      return nil if not client_ids.include? body['audience']
      # TODO log bad client

      # TODO possibly validate scope

      if identity = Identity.find_by_uid(body['user_id'])
        user = identity.user
        logger.info "Authenticated #{user.id} as #{identity.email}"
        return user
      end
      return nil
    rescue RestClient::Exception => e
      logger.warn e
      render :text => "OAuth validation failed", :status => :unauthorized
      return nil
    end
  end

  def require_login_or_oauth!
    if "application/json" == request.format
      user = find_oauth_user
      if user
        sign_in(:user, user)
        session[:user_id] = user.id
      else
        redirect_to root_url, status: 401
      end
    else
      authenticate_user!
    end
  end
end
ライセンス: CC-BY-SA帰属
所属していません StackOverflow
scroll top