-
-
Save easantillan/54b158ea960812e6569b317eefe966f4 to your computer and use it in GitHub Desktop.
Revisions
-
jesster2k10 revised this gist
Apr 26, 2020 . 1 changed file with 6 additions and 1 deletion.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -97,4 +97,9 @@ gem 'google-id-token', git: 'https://github.com/google/google-id-token.git' gem 'koala' ``` > Note: Make sure to install the google-id-token gem from the repo (as of 26th April 2020) in order to support passing an array of client_ids ### Specs/Tests The tests can be found [here](https://gist.github.com/jesster2k10/4a547c05df38d07a66b10f0b83546838) -
jesster2k10 revised this gist
Apr 26, 2020 . 1 changed file with 14 additions and 0 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -34,6 +34,8 @@ So there's an advantage to using this setup, it's a lot easier to work with a si However, if you are using a traditional Rails Application (not a rails api), you should totally go with Omniauth and save all the hastle. The code is quite modular so it will be extemely easy to add support for another login provider if needed. It's a matter of creating another `Provider::Base` super class and including the provider name in your `Identity.providers` array. ## Basic Structure There are quite a lot of files in this, each with different roles. @@ -84,3 +86,15 @@ The identities table is structured like so: add_index :identities, %i[provider uid], unique: true ``` ### Dependencies Right now, I've implemented only two providers - Google and Facebook auth. The dependencies that I'm using are ```ruby gem 'google-id-token', git: 'https://github.com/google/google-id-token.git' gem 'koala' ``` > Note: Make sure to install the google-id-token gem from the repo (as of 26th April 2020) in order to support passing an array of client_ids -
jesster2k10 revised this gist
Apr 26, 2020 . 7 changed files with 339 additions and 2 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -38,7 +38,49 @@ However, if you are using a traditional Rails Application (not a rails api), you There are quite a lot of files in this, each with different roles. ### base_provider.rb An abstract class that serves as a base for all the different login providers making it easy to implement new providers if needed. Provides common extractions for methods. ### facebook_provider.rb A provider class for handling login with facebook. > Note: The fields requested must match those requested on the native side otherwise you're going to get a run-time error becuase the granted acccess token won't have adequate permissions ### google_provider.rb A provider class for handling login with google. You need to provide an array of client_ids for the different clients you will have needing to sign in e.g. iOS App, Android App, React App, Windows App etc. In this example, I only have one (ios_client_id) ### provider_credentials.rb Just an object to handle the credentials (access token/refresh token) ### provider_info.rb This provides a common interface for the User Information retrieved from the external APIs. Similar to the Omniauth env['auth'] ### identity.rb So each user has what I call an `Identity` which is just an external login. If you want your app to only support one external login e.g. facebook only, this identities table wouldn't be needed as you could just store the `uid/provider/access_token` on the user model. The identities table is structured like so: ```ruby create_table :identities do |t| t.belongs_to :user, null: false, foreign_key: true t.string :provider, null: false t.string :uid, null: false t.string :access_token t.string :refresh_token t.jsonb :auth_hash, default: {} t.timestamps end add_index :identities, %i[provider uid], unique: true ``` This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,33 @@ module External module Provider class Facebook < Base def info ProviderInfo.new do |i| i.provider = External::Provider::Facebook.name i.date_format = '%m/%d/%Y' i.uid = raw_info[:id] i.email = raw_info[:email] i.birthday = raw_info[:birthday] i.first_name = raw_info[:first_name] i.last_name = raw_info[:last_name] i.name = raw_info[:name] end end def retrieve_client Koala::Facebook::API.new access_token end def retrieve_user_info client.get_object( :me, fields: %w[id email birthday first_name last_name] ) end def self.name :facebook end end end end This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,42 @@ module External module Provider class Google < Base def info ProviderInfo.new do |i| i.provider = External::Provider::Google.name i.avatar = raw_info[:picture] i.first_name = raw_info[:given_name] i.last_name = raw_info[:family_name] i.email = raw_info[:email] i.name = raw_info[:name] i.uid = raw_info[:sub] end end def retrieve_client GoogleIDToken::Validator.new end def retrieve_user_info client.check( id_token, client_ids ) end def self.name :google end private # The different clients your app needs to authenticate agains # E.g. iOS, Android, Web etc. def client_ids [Rails.application.credentials.google[:ios_client_id]] end alias id_token access_token end end end This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,99 @@ class Api::V1::IdentitiesController < ApplicationController before_action :authenticate_for_identity! attr_reader :provider, :identity, :user, :access_token, :refresh_token # POST /identities/:provider def provider_create @provider = params[:provider] # We want to make sure we support the provider if Identity.valid_provider?(provider) set_identity return unless set_user # If the identity exists, we want to update it if identity.present? identity.update identity_params # Create a new identity for this user else user.identities.create identity_params end # Finally we want to create a new auth token pair for the user issue_tokens # Render the user object and the auth tokens render_json data: { token: { access_token: access_token, refresh_token: refresh_token }, user: UserBlueprint.render_as_json(user) }, status: :created else # Render an error saying the provider is not supported render_unsupported_provider_error end end def set_identity @identity = Identity.where(provider: provider, uid: external.info.uid).first end def set_user # The user is already logged in (access_token present in request) # We want to set the user to the current user if current_user.present? @user = current_user # The user has signed in with this provider before # Return the existing account elsif identity.present? @user = identity.user # There is an existing user account with the same email & the user is not # logged in. We don't want to create an account for safety. # Tell the user to sign in to their account and link it there elsif User.where(email: external.info.email).any? render_existing_account_error provider: provider return false # The user has never signed in before, we want to create a new account from # the external info object else @user = User.create_from_provider_info(external.info) end true end # We want to update the identity object with the new uid and provider. # Storing the ccess_token or refresh_token is optional since that # can be delt with on the native clients. def identity_params { provider: external.info.provider, uid: external.info.uid, access_token: external.credentials.access_token, refresh_token: external.credentials.refresh_token, auth_hash: external.info.to_h } end def issue_tokens @access_token, @refresh_token = Jwt::Issuer.call user response.set_header 'Access-token', access_token response.set_header 'Refresh-token', refresh_token end private # Returns the External::Provider object for the current provider # We can access the user info object and raw_info hash from here def external @external ||= External::Provider::Base .for(provider) .new(create_params[:token]) end def create_params params.require(:data).permit(:token) end end This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,23 @@ class Identity < ApplicationRecord belongs_to :user PROVIDERS = %w[facebook google].freeze PROVIDERS.each do |provider| scope provider, -> { where(provider: provider) } end validates :uid, uniqueness: { scope: :provider }, presence: true validates :provider, inclusion: { in: PROVIDERS, message: 'this provider is not supported' }, presence: true def self.providers PROVIDERS end def self.valid_provider?(provider) providers.include?(provider.to_s) end end This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,20 @@ module External class ProviderCredentials attr_accessor :access_token, :refresh_token, :expires, :expires_at def initialize yield self if block_given? end def to_h { access_token: access_token, refresh_token: refresh_token, expires: expires, expires_at: expires_at } end alias hash to_h end end This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,78 @@ module External class ProviderInfo attr_accessor :uid, :email, :username, :date_format, :avatar, :provider attr_writer :birthday, :first_name, :last_name, :name def initialize yield self if block_given? end def first_name raw_name = @first_name if raw_name.present? raw_name elsif @name.present? split_name = @name.split(' ') if split_name.length > 1 @name.split(' ')[0..-2].join(' ') else split_name.first end end end def last_name raw_name = @last_name if raw_name.present? raw_name elsif @name.present? split_name = @name.split(' ') return split_name if split_name.length > 1 end end def birthday raw_value = @birthday if raw_value.is_a?(String) if date_format.present? Date.strptime raw_value, date_format else Date.parse raw_value end elsif raw_value.is_a?(Integer) Time.at(raw_value).to_date else raw_value end end def name raw_name = @name if raw_name.present? raw_name elsif @first_name.present? && @last_name.present? "#{@first_name} #{@last_name}" elsif @first_name.present? @first_name end end def to_h { uid: uid, email: email, username: username, birthday: birthday, first_name: first_name, last_name: last_name, name: name } end alias hash to_h end end -
jesster2k10 revised this gist
Apr 26, 2020 . 2 changed files with 77 additions and 1 deletion.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -28,4 +28,17 @@ In comparison, if you were using Omniauth you would have to: - The user is redirected back to your backend - The backend generates a user account - Then it generates an auth token (assuming you're using JWT auth) - The backend redirects the user back to the native app with the access token. So there's an advantage to using this setup, it's a lot easier to work with a single JSON API endpoint than it is to work with a bunch of redirects in a web view. However, if you are using a traditional Rails Application (not a rails api), you should totally go with Omniauth and save all the hastle. ## Basic Structure There are quite a lot of files in this, each with different roles. ### base_provider.rb An abstract class that serves as a base for all the different login providers making it easy to implement new providers if needed. Provides common extractions for methods. This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,63 @@ module External # A base class for handling external logins module Provider class Base attr_reader :access_token, :refresh_token, :client def initialize(access_token, refresh_token = nil) handle_missing_access_token unless access_token.present? @access_token = access_token @refresh_token = refresh_token @client = retrieve_client end def self.for(provider) class_name = provider.to_s.classify "External::Provider::#{class_name}".safe_constantize end # The name of the provider def self.name :base end # The user's info def info handle_unimplmeneted_method end # The user's credentials def credentials ProviderCredentials.new do |c| c.access_token = access_token c.refresh_token = refresh_token end end # The raw info returned from the hash def raw_info @raw_info ||= retrieve_user_info.deep_symbolize_keys end # Returns the raw info hash from the client def retrieve_user_info handle_unimplmeneted_method end def retrieve_client handle_unimplmeneted_method end private def handle_missing_access_token raise Errors::Domain, status: 422, message: 'missing token' end def handle_unimplmeneted_method(method = :method) raise StandardError, "Expected super class to override #{method}" end end end end -
jesster2k10 created this gist
Apr 26, 2020 .There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,31 @@ # Rails API-Only Social Login This is another piece of code I've extrapolated from a Ruby on Rails project I'm currently working on. The code implmenets social login with a RoR API-based application, targeted at API clients. The setup does not involve any browser-redirects or sessions as you would have to use working with Omniauth. Instead, what it does is takes an access_token generated on client-side SDKs, retireves user info from the access token and creates a new user and `Identity` in the database. This setup works with native applications as described in the [Google iOS Sign In Docs (see Authenticating with a backend server)](https://developers.google.com/identity/sign-in/ios/backend-auth) A quote from that page pretty much sums up how this works: > After you have verified the token, check if the user is already in your user database. If so, establish an authenticated session for the user. If the user isn't yet in your user database, create a new user record from the information in the ID token payload, and establish a session for the user. You can prompt the user for any additional profile information you require when you detect a newly created user in your app. The basic flow of the login is: - You sign in using the respective native sdks on your mobile/js clients - An access_token/id_token is retireved from the mobile SDKs - A request is made to your Rails Application `POST /identities/:provider` with the token - The server then fetches the user data from the token after validating it with the provider - The server then creates a user profile & `Identity` based on that information. In comparison, if you were using Omniauth you would have to: - Open up a web view in your mobile app linking to your backend - Your user is redirected from your backend to the external provider - The user signs in with the external provider - The user is redirected back to your backend - The backend generates a user account - Then it generates an auth token (assuming you're using JWT auth) - The backend redirects the user back to the native app with the access token.