So I logged in with Google and it hits my callback url with a code parameter.
Here is how I initiate my client, I'm using oauth2 gem.
def oauth_client(channel_name)
file = YAML.load_file("#{Rails.root}/config/oauth_credentials.yml")
client_id = file['oauth_credentials'][channel_name]['client_id']
client_secret = file['oauth_credentials'][channel_name]['secret']
site = file['oauth_credentials'][channel_name]['site']
OAuth2::Client.new(client_id, client_secret, site: site, authorize_url: "/o/oauth2/auth", connection_opts: { params: { scope: "https://www.googleapis.com/auth/adsense https://www.googleapis.com/auth/analytics.readonly" } })
end
def oauth_url_for(channel_name)
client = oauth_client(channel_name)
client.auth_code.authorize_url(:redirect_uri => oauth_callback_url(channel: channel_name))
end
Here's my controller
class Oauth2Controller < ApplicationController
include ApplicationHelper
def callback
token = oauth_client(params[:channel]).auth_code.get_token(params[:code], :redirect_uri => oauth_callback_url(channel: params[:channel]))
current_user.connections.create!(channel: params[:channel], token: token)
render text: request.inspect
end
end
Unfortunately I can't get_token due to a response from google saying The page you requested is invalid.
It doesn't look like you're defining the token_url value in your initialization of the client, or later. It is "o/oauth2/token". By default "/oauth/token" is used, so that's likely the cause of your "page you requested is invalid" error.
Source:
http://rubydoc.info/gems/oauth2/0.8.0/OAuth2/Client#initialize-instance_method
Related
I spent hours looking for a solution to my problem without luck.
I created a Rails app that interacts with Google Calendar API and, following a guide, I was able to make it work. Then I shut down my local server and ran it again after 2/3 hours and I started receiving this error when I make a request to the API:
Signet::AuthorizationError (Unexpected error: #<ArgumentError: Missing authorization code.>)
I figured out that the problem is that the token is expired, 'cause I can't understand why it was working and then not anymore.
This is my controller:
require 'google/api_client/client_secrets.rb'
require 'google/apis/calendar_v3'
CALENDAR_ID = Here I have my Calendar_ID
GOOGLE_CLIENT_ID = Here I have my Google_Client_ID
GOOGLE_CLIENT_SECRET = Here I have my Google_Client_Secret
class CalendarController < ApplicationController
def calendars
client = Signet::OAuth2::Client.new(client_options)
client.update!(session[:authorization])
service = Google::Apis::CalendarV3::CalendarService.new
service.authorization = client
#calendar_list = service.list_calendar_lists
rescue Google::Apis::AuthorizationError
response = client.refresh!
session[:authorization] = session[:authorization].merge(response)
retry
end
def redirect
client = Signet::OAuth2::Client.new(client_options)
redirect_to client.authorization_uri.to_s, allow_other_host: true
end
def callback
client = Signet::OAuth2::Client.new(client_options)
client.code = params[:code]
response = client.fetch_access_token!
session[:authorization] = response
redirect_to calendars_url
end
private
def client_options
{
client_id: GOOGLE_CLIENT_ID,
client_secret: GOOGLE_CLIENT_SECRET,
authorization_uri: 'https://accounts.google.com/o/oauth2/auth',
token_credential_uri: 'https://accounts.google.com/o/oauth2/token',
scope: Google::Apis::CalendarV3::AUTH_CALENDAR,
redirect_uri: callback_url
}
end
end
Then my routes:
get '/redirect', to: 'calendar#redirect', as: 'redirect'
get '/callback', to: 'calendar#callback', as: 'callback'
get '/calendars', to: 'calendar#calendars', as: 'calendars'
What can I do to solve my issue? I'm a newbie on Rails, so it's not quite simple for me
I use the gem jwt、devise to build a user login system,
I generate a model Authentication to check the token exist or not.
follow this code:
models/authentication.rb
class Authentication < ApplicationRecord
def self.generate_access_token(email)
payload = {:email => email}
secret = 'secret'
token = JWT.encode payload, secret, 'HS256'
return token
end
end
controllers/user/sessions_controller.rb
def create
user = User.where(email: params[:email]).first
if user&.valid_password?(params[:password])
#token = Authentication.generate_access_token(user.email)
Authentication.create(access_token: #token)
authentications = {token: #token, email: user.email}
render json: authentications, status: :created
else
head(:unauthorized)
end
end
when I do a post request to user/sessions I will get token and user email and store it in localstorage of client, and help me to check the token is valid.
follow this code:
def authenticate_token
token = Authentication.find_by_access_token(params[:token])
head :unauthorized unless token
end
In my question, are there ways to let token don't need to store into database?
You can decode the token and get the email stored in it, and find user by that email.
Suppose you carry the token in the Authorization header, like
Authorization: Bearer <token>
then you can define a before_action to do this:
class ApplicationController < ActionController::API
before_action :authenticate_token
def authenticate_token
token = request.headers['Authorization'].to_s =~ /^Bearer (.*)$/i && $1
return head :unauthorized unless token
payload = JWT.decode(token, 'secret', true, algorithm: 'HS256')
user = User.find_by(email: payload['email'])
return head :unauthorized unless user
# TODO set the `user` as current_user
# How to patch devise's `current_user` helper is another story
end
end
If I were you, I would put user ID in the token, not email, because ID is shorter, and faster to lookup from database, and it exposes nothing personal to the internet (note that JWT is not encrypted. It's just signed).
Or you can skip all these messy things by just using knock instead of devise.
I am getting Signet::AuthorizationError in LoginController#callback and Authorization failed. Server message: { "error" : "redirect_uri_mismatch" } on the line auth_client.fetch_access_token! when I click "Allow" on the OAuth screen and execute my callback method during the OAuth process.
I have checked out this: Google OAuth 2 authorization - Error: redirect_uri_mismatch, but I still can't figure it out.
I am trying to get a login with Google set up like this:
User goes to /login and clicks on the "sign in with google" button.
The Google login prompt comes up, user signs in with Gmail, and then gets redirected to /home.
I have the following uris in my web application credentials in Google consoles:
http://localhost:3000
http://localhost:3000/login/callback
http://localhost/login/callback
http://localhost
routes.rb
get '/home' => 'home#index'
get '/login' => 'login#prompt'
get '/login/callback' => 'login#callback'
login_controller.rb
require 'google/api_client/client_secrets'
class LoginController < ApplicationController
GOOGLE_CLIENT_SECRET_FILE = Rails.root.join('config/google_oauth2_secret.json')
def prompt
if session[:credentials]
redirect_to '/home'
else
auth_client = get_auth_client
auth_client.update!(
:scope => ['profile', 'email'],
:redirect_uri => 'http://localhost:3000/login/callback'
)
#auth_uri = auth_client.authorization_uri.to_s
render layout: false
end
end
def callback
auth_client = get_auth_client
auth_client.code = request['code']
auth_client.fetch_access_token!
auth_client.client_secret = nil
session[:credentials] = auth_client.to_json
redirect_to '/home'
end
private
def get_auth_client
Google::APIClient::ClientSecrets.load(GOOGLE_CLIENT_SECRET_FILE).to_authorization
end
end
I also have a concern. In my prompt method, how do I verify that session[:credentials] is the correct session code? Couldn't anyone just put some bogus string into the credentials session and gain access?
I have been following this guide: https://developers.google.com/api-client-library/ruby/auth/web-app
This happens because localhost is not a valid domain name. Instead of using localhost, you need to use lvh.me
In Google consoles the url will become
http://lvh.me:3000
http://lvh.me:3000/login/callback
Trying accessing your application in the browser using http://lvh.me:3000 instead of http://localhost:3000
lvh.me is a valid domain name with is pointing to 127.0.0.1
I realize the issue is I am setting the scope in the prompt function when I am generating the auth uri but in my callback function, I am not setting the scope.
It should be like this:
def callback
auth_client = get_auth_client
auth_client.update!(
:scope => ['profile', 'email'],
:redirect_uri => 'http://localhost:3000/login/callback'
)
auth_client.code = request['code']
auth_client.fetch_access_token!
auth_client.client_secret = nil
session[:credentials] = auth_client.to_json
redirect_to '/home'
end
Can you give an advice or recommend some resources related to this topic? I understand how to it in a theory. But I also heard about jwt etc. What are the best practices to implement device/angular/rails role based auth/registration?
The short answer is to read this blog post which goes into details of how the concept is minimally implemented
This would be a long code answer, but I plan to write separate blog post on how to implement it in much more details...
but for now, here is how I implemented it in some project...
First the angular app part, you can use something like Satellizer which plays nicely...
here is the angular auth module in the front-end app
# coffeescript
config = (
$authProvider
$stateProvider
) ->
$authProvider.httpInterceptor = true # to automatically add the headers for auth
$authProvider.baseUrl = "http://path.to.your.api/"
$authProvider.loginRedirect = '/profile' # front-end route after login
$authProvider.logoutRedirect = '/' # front-end route after logout
$authProvider.signupRedirect = '/sign_in'
$authProvider.loginUrl = '/auth/sign_in' # api route for sign_in
$authProvider.signupUrl = '/auth/sign_up' # api route for sign_up
$authProvider.loginRoute = 'sign_in' # front-end route for login
$authProvider.signupRoute = 'sign_up' # front-end route for sign_up
$authProvider.signoutRoute = 'sign_out' # front-end route for sign_out
$authProvider.tokenRoot = 'data'
$authProvider.tokenName = 'token'
$authProvider.tokenPrefix = 'front-end-prefix-in-localstorage'
$authProvider.authHeader = 'Authorization'
$authProvider.authToken = 'Bearer'
$authProvider.storage = 'localStorage'
# state configurations for the routes
$stateProvider
.state 'auth',
url: '/'
abstract: true
templateUrl: 'modules/auth/auth.html'
data:
permissions:
only: ['guest']
redirectTo: 'profile'
.state 'auth.sign_up',
url: $authProvider.signupRoute
views:
'sign_up#auth':
templateUrl: 'modules/auth/sign_up.html'
controller: 'AuthenticationCtrl'
controllerAs: 'vm'
.state 'auth.sign_in',
url: $authProvider.loginRoute
views:
'sign_in#auth':
templateUrl: 'modules/auth/sign_in.html'
controller: 'AuthenticationCtrl'
controllerAs: 'vm'
this is the basic configurations for satellizer... as for the authentication controller... it's something like following
#signIn = (email, password, remember_me) ->
$auth.login
email: email
password: password
remember_me: remember_me
.then(success, error)
return
#signUp = (name, email, password) ->
$auth.signup
name: name
email: email
password: password
.then(success, error)
return
this is the basics for authenticating
as for the backend (RoR API) you should first allow CORS for the front-end app. and add gem 'jwt' to your gemfile.
second implement the API controller and the authentication controller
for example it might look something like the following
class Api::V1::ApiController < ApplicationController
# The API responds only to JSON
respond_to :json
before_action :authenticate_user!
protected
def authenticate_user!
http_authorization_header?
authenticate_request
set_current_user
end
# Bad Request if http authorization header missing
def http_authorization_header?
fail BadRequestError, 'errors.auth.missing_header' unless authorization_header
true
end
def authenticate_request
decoded_token ||= AuthenticationToken.decode(authorization_header)
#auth_token ||= AuthenticationToken.where(id: decoded_token['id']).
first unless decoded_token.nil?
fail UnauthorizedError, 'errors.auth.invalid_token' if #auth_token.nil?
end
def set_current_user
#current_user ||= #auth_token.user
end
# JWT's are stored in the Authorization header using this format:
# Bearer some_random_string.encoded_payload.another_random_string
def authorization_header
return #authorization_header if defined? #authorization_header
#authorization_header =
begin
if request.headers['Authorization'].present?
request.headers['Authorization'].split(' ').last
else
nil
end
end
end
end
class Api::V1::AuthenticationsController < Api::V1::ApiController
skip_before_action :authenticate_user!, only: [:sign_up, :sign_in]
def sign_in
# getting the current user from sign in request
#current_user ||= User.find_by_credentials(auth_params)
fail UnauthorizedError, 'errors.auth.invalid_credentials' unless #current_user
generate_auth_token(auth_params)
render :authentication, status: 201
end
def sign_out
# this auth token is assigned via api controller from headers
#auth_token.destroy!
head status: 204
end
def generate_auth_token(params)
#auth_token = AuthenticationToken.generate(#current_user, params[:remember_me])
end
end
The AuthenticationToken is a model used to keep track of the JWT tokens ( for session management like facebook)
here is the implementation for the AuthenticationToken model
class AuthenticationToken < ActiveRecord::Base
## Relations
belongs_to :user
## JWT wrappers
def self.encode(payload)
AuthToken.encode(payload)
end
def self.decode(token)
AuthToken.decode(token)
end
# generate and save new authentication token for the user
def self.generate(user, remember_me = false)
#auth_token = user.authentication_tokens.create
#auth_token.token = AuthToken.generate(#auth_token.id, remember_me)
#auth_token.save!
#auth_token
end
# check if a token can be used or not
# used by background job to clear the authentication collection
def expired?
AuthToken.decode(token).nil?
end
end
it uses a wrapper called AuthToken which wraps the JWT functionality
here is it's implementation
# wrapper around JWT to encapsulate it's code
# and exception handling and don't polute the AuthenticationToken model
class AuthToken
def self.encode(payload)
JWT.encode(payload, Rails.application.secrets.secret_key_base)
end
def self.decode(token)
payload = JWT.decode(token, Rails.application.secrets.secret_key_base)[0]
rescue JWT::ExpiredSignature
# It will raise an error if it is not a token that was generated
# with our secret key or if the user changes the contents of the payload
Rails.logger.info "Expired Token"
nil
rescue
Rails.logger.warn "Invalid Token"
nil
end
def self.generate(token_id, remember_me = false)
exp = remember_me ? 6.months.from_now : 6.hours.from_now
payload = { id: token_id.to_s, exp: exp.to_i }
self.encode(payload)
end
end
I am developing a Rails 4 web application and i am planning to authenticate a user from my Windows Azure AD.
For that i have subscribed to Windows Azure and created Active Directory. I then created an application inside AD to get Client and Secret ID to access API exposed by windows Azure from my Rails web application.
For this i am planning to use Devise gem . Is this the right solution or is there any other libraries available to achieve this.
Any help is appreciated.
Not sure if you're still looking for anything, but this Gist has the requisite request code using the OAuth2 gem. If you were ultimately able to get things working with another means (Devise, etc.), I'd also be interested to know what you did.
Here's the code from the Gist as packaged into a controller by omercs:
require 'oauth2'
class WelcomeController < ApplicationController
# You need to configure a tenant at Azure Active Directory(AAD) to register web app and web service app
# You will need two entries for these app at the AAD portal
# You will put clientid and clientsecret for your web app here
# ResourceId is the webservice that you registered
# RedirectUri is registered for your web app
CLIENT_ID = 'b6a42...'
CLIENT_SECRET = 'TSbx..'
AUTHORITY = 'https://login.windows.net/'
AUTHORIZE_URL = "/yourtenant.onmicrosoft.com/oauth2/authorize"
TOKEN_URL = "/yourtenant.onmicrosoft.com/oauth2/token"
RESOURCE_ID = 'https://yourtenant.onmicrosoft.com/AllHandsTry' #ResourceId or ResourceURI that you registered at Azure Active Directory
REDIRECT_URI = 'http://localhost:3000/welcome/callback'
def index
update_token
if session['access_token']
# show main page and use token
redirect_to welcome_use_token_path
else
# start authorization
client = get_client
a = client.auth_code.authorize_url(:client_id => CLIENT_ID, :resource => RESOURCE_ID, :redirect_uri => REDIRECT_URI)
redirect_to(a)
end
end
def callback
begin
#code = params[:code]
client = get_client
# post token to mobile service api
#token = client.auth_code.get_token(CGI.escape(#code), :redirect_uri => REDIRECT_URI)
# id_token token.params["id_token"]
#multi resource token token.params["resource"]
token = client.auth_code.get_token(#code, :redirect_uri => REDIRECT_URI, )
session['access_token'] = token.token
session['refresh_token'] = token.refresh_token
session['expire_at'] = token.expire_at
session['instance_url'] = token.params['instance_url']
redirect '/'
rescue => exception
output = '<html><body><p>'
output += "Exception: #{exception.message}<br/>"+exception.backtrace.join('<br/>')
output += '</p></body></html>'
end
end
def update_token
puts "update token inside"
token = session['access_token']
refresh_token = session['refresh_token']
expire_at = session['expire_at']
#access_token = OAuth2::AccessToken.from_hash(get_client, { :access_token => token, :refresh_token => refresh_token, :expire_at => expire_at, :header_format => 'Bearer %s' } )
if #access_token.expired?
puts "refresh token"
#access_token = #access_token.refresh!;
session['access_token'] = #access_token.token
session['refresh_token'] = #access_token.refresh_token
session['expire_at'] = #access_token.expire_at
session['instance_url'] = #access_token.params['instance_url']
end
end
# send post request to webservice to send token and create a post request
def use_token
# we got the token and now it will posted to the web service in the header
# you can specify additional headers as well
# token is included by default
update_token
conn = Faraday.new(:url => 'https://yoursite.azurewebsites.net/') do |faraday|
faraday.request :url_encoded # form-encode POST params
faraday.response :logger # log requests to STDOUT
faraday.adapter Faraday.default_adapter # make requests with Net::HTTP
end
response = conn.get do |req|
req.url '/api/WorkItem'
req.headers['Content-Type'] = 'application/json'
req.headers['Authorization'] = 'Bearer '+#access_token.token
end
#out = response.body
end
def get_client
client = OAuth2::Client.new(CLIENT_ID, CLIENT_SECRET, :site => AUTHORITY, :authorize_url => AUTHORIZE_URL, :token_url => TOKEN_URL )
client
end
end