Skip to content

Commit

Permalink
Feature: Auth0 support (#1245)
Browse files Browse the repository at this point in the history
* Add Auth0 support for users signup and signin
  • Loading branch information
chumaknadya authored Feb 12, 2021
1 parent 1a412f1 commit 873bf70
Show file tree
Hide file tree
Showing 15 changed files with 320 additions and 26 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,7 @@

# Ignore master key for decrypting credentials and more.
/config/master.key
/config/rsa-key.pub
/config/rsa-key

.idea/
91 changes: 68 additions & 23 deletions app/api/v2/identity/sessions.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,29 +5,9 @@
module API::V2
module Identity
class Sessions < Grape::API
desc 'Session related routes'
resource :sessions do
desc 'Start a new session',
failure: [
{ code: 400, message: 'Required params are empty' },
{ code: 404, message: 'Record is not found' }
],
success: API::V2::Entities::UserWithFullInfo
params do
requires :email
requires :password
optional :captcha_response,
types: { value: [String, Hash], message: 'identity.session.invalid_captcha_format' },
desc: 'Response from captcha widget'
optional :otp_code,
type: String,
desc: 'Code from Google Authenticator'
end
post do
verify_captcha!(response: params['captcha_response'], endpoint: 'session_create')

declared_params = declared(params, include_missing: false)
user = User.find_by(email: declared_params[:email])
helpers do
def get_user(email)
user = User.find_by(email: email)
error!({ errors: ['identity.session.invalid_params'] }, 401) unless user

if user.state == 'banned'
Expand All @@ -45,6 +25,32 @@ class Sessions < Grape::API
login_error!(reason: 'Your account is not active', error_code: 401,
user: user.id, action: 'login', result: 'failed', error_text: 'not_active')
end
user
end
end

desc 'Session related routes'
resource :sessions do
desc 'Start a new session',
failure: [
{ code: 400, message: 'Required params are empty' },
{ code: 404, message: 'Record is not found' }
]
params do
requires :email
requires :password
optional :captcha_response,
types: { value: [String, Hash], message: 'identity.session.invalid_captcha_format' },
desc: 'Response from captcha widget'
optional :otp_code,
type: String,
desc: 'Code from Google Authenticator'
end
post do
verify_captcha!(response: params['captcha_response'], endpoint: 'session_create')

declared_params = declared(params, include_missing: false)
user = get_user(declared_params[:email])

unless user.authenticate(declared_params[:password])
login_error!(reason: 'Invalid Email or Password', error_code: 401, user: user.id,
Expand Down Expand Up @@ -89,6 +95,45 @@ class Sessions < Grape::API
session.destroy
status(200)
end

desc 'Auth0 authentication by id_token',
success: { code: 200, message: 'User authenticated' },
failure: [
{ code: 400, message: 'Required params are empty' },
{ code: 404, message: 'Record is not found' }
]
params do
requires :id_token,
type: String,
allow_blank: false,
desc: 'ID Token'
end
post '/auth0' do
begin
# Decode ID token to get user info
claims = Barong::Auth0::JWT.verify(params[:id_token]).first
error!({ errors: ['identity.session.auth0.invalid_params'] }, 401) unless claims.key?('email')
user = User.find_by(email: claims['email'])

This comment has been minimized.

Copy link
@dapi

dapi Mar 2, 2021

Hi! @chumaknadya @calj

Good job!

Pay your attention. It is bad practice to identify user by its email. Email could be changed. Use sub key of JWT instead.

Best flow is:

  1. Find user by sub. If it is found use them and return session.
  2. Find user by email with empty sub. Update its sub and return session.
  3. Create user with email and sub. Process situation where user with such email already exists.

BTW Why don't your make standard oauth2 authorisation callbacks? auth2 support it too.

👍


# If there is no user in platform and user email verified from id_token
# system will create user
if user.blank? && claims['email_verified']
user = User.create!(email: claims['email'], state: 'active')
user.labels.create!(scope: 'private', key: 'email', value: 'verified')
elsif claims['email_verified'] == false
error!({ errors: ['identity.session.auth0.invalid_params'] }, 401) unless user
end

activity_record(user: user.id, action: 'login', result: 'succeed', topic: 'session')
csrf_token = open_session(user)
publish_session_create(user)

present user, with: API::V2::Entities::UserWithFullInfo, csrf_token: csrf_token
rescue StandardError => e
report_exception(e)
error!({ errors: ['identity.session.auth0.invalid_params'] }, 422)
end
end
end
end
end
Expand Down
8 changes: 8 additions & 0 deletions app/api/v2/public/general.rb
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,14 @@ class General < Grape::API
password_regexp: Barong::App.config.password_regexp
}.compact
end

desc 'Get auth0 configuration'
get '/configs/auth0' do
{
auth0_domain: Barong::App.config.auth0_domain,
auth0_client_id: Barong::App.config.auth0_client_id
}.compact
end
end
end
end
5 changes: 5 additions & 0 deletions app/models/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,14 @@ class User < ApplicationRecord
{ key: 'document', value: ['pending', 'replaced'], scope: 'private' }) }

before_validation :assign_uid
before_validation :generate_password, on: :create
after_update :disable_api_keys
after_update :disable_service_accounts

def generate_password
self.password = SecureRandom.base64(30) unless password
end

def validate_pass!
return unless (new_record? && password.present?) || password.present?

Expand Down
4 changes: 4 additions & 0 deletions config/initializers/barong.rb
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,10 @@
config.set(:kycaid_authorization_token, '')
config.set(:kycaid_sandbox_mode, 'true', type: :bool)
config.set(:kycaid_api_endpoint, 'https://api.kycaid.com/')

# Auth0 configuration -----------------------------------------------
config.set(:auth0_domain, '')
config.set(:auth0_client_id, '')
end

# KYCAID configuring
Expand Down
7 changes: 7 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,13 @@ More details in [twilio configuration](#twilio-configuration)
### Sentry configuration
| `barong_sentry_dsn_backend` | ~ | valid host url | Sentry SDK client key |

### Auth0 configuration

| Env name | Default value | Possible values | Description |
| ---------- | :------: |:------: |---------------------------------- |
|`auth0_domain`| - | any string value | auth0 Domain name (without https://) |
|`auth0_client_id`| - | any string value | the client_id of your auth0 application |

### SMTP configuration
| Env name | Default value | Possible values | Description |
| ---------- | ------ |-------------------------|---------------------------------- |
Expand Down
48 changes: 48 additions & 0 deletions docs/general/auth0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Auth0 integration

## How to create an application

When you signed up for Auth0, a new application was created for you, or you could have created a new one (the most appropriate application for our structure is Single Page Application).
![Application](../images/auth0_dashboard.png)

This comment has been minimized.

Copy link
@dapi

dapi Mar 3, 2021

@chumaknadya hi!

There are no such image


You will need some details about that application to communicate with Auth0. You can get these details from the Application Settings section in the Auth0 dashboard.

![Settings](../images/auth0_settings.png)

This comment has been minimized.

Copy link
@dapi

dapi Mar 3, 2021

There are no such images. Where I can get it?

Thank you!


You should put `auth0_domain` from the Domain field and `auth0_client_id` from the Client ID field

For a single page application better to use authorization code flow with proof key for code exchange.

## Authorization Code Flow with Proof Key for Code Exchange (PKCE)
When public clients request Access Tokens, some additional security concerns are posed that are not mitigated by the Authorization Code Flow alone. This is because single-page apps cannot securely store a Client Secret because their entire source is available to the browser.


### How it works
![PKCE](../images/auth0_pkce.png)
Because the PKCE-enhanced Authorization Code Flow builds upon the standard Authorization Code Flow, the steps are very similar.

1. The user clicks Login within the application.

2. Auth0's SDK creates a cryptographically-random code_verifier and from this generates a code_challenge.

3. Auth0's SDK redirects the user to the Auth0 Authorization Server (/authorize endpoint) along with the code_challenge.

4. Your Auth0 Authorization Server redirects the user to the login and authorization prompt.

5. The user authenticates using one of the configured login options and may see a consent page listing the permissions Auth0 will give to the application.

6. Your Auth0 Authorization Server stores the code_challenge and redirects the user back to the application with an authorization code, which is good for one use.

7. Auth0's SDK sends this code and the code_verifier (created in step 2) to the Auth0 Authorization Server (/oauth/token endpoint).

8. Your Auth0 Authorization Server verifies the code_challenge and code_verifier.

9. Your Auth0 Authorization Server responds with an ID Token and Access Token (and optionally, a Refresh Token).

10. Your application can use the Access Token to call an API to access information about the user.

11. The API responds with requested data.

You can try to call your [API using the authorization Code Flow with PKCE](https://auth0.com/docs/flows/call-your-api-using-the-authorization-code-flow-with-pkce).

Also you can find a link with [Authentication API description](https://auth0.com/docs/api/authentication#introduction) here.
Binary file added docs/images/auth0_dashboard.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/auth0_pkce.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/auth0_settings.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
34 changes: 34 additions & 0 deletions lib/barong/auth0/jwt.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# frozen_string_literal: true

module Barong
module Auth0
class JWT
def self.verify(token)
::JWT.decode(token,
nil,
true, # Verify the signature of this token
algorithms: 'RS256',
iss: "https://#{Barong::App.config.auth0_domain}/",
verify_iss: true,
aud: Barong::App.config.auth0_client_id,
verify_aud: true
) do |header|
jwks_hash[header['kid']]
end
end

def self.jwks_hash
uri = "https://#{Barong::App.config.auth0_domain}/.well-known/jwks.json"
jwks_raw = Net::HTTP.get URI(uri)
jwks_keys = Array(JSON.parse(jwks_raw)['keys'])
Hash[jwks_keys.map do |k|
[
k['kid'],
OpenSSL::X509::Certificate.new(Base64.decode64(k['x5c'].first)).public_key
]
end
]
end
end
end
end
2 changes: 1 addition & 1 deletion lib/barong/event_api.rb
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ def notify(partial_event_name, event_payload)
tokens << partial_event_name.to_s
full_event_name = tokens.join('.')

EventAPI.notify(full_event_name, event_payload)
::EventAPI.notify(full_event_name, event_payload)
end

def notify_record_created
Expand Down
2 changes: 1 addition & 1 deletion lib/barong/jwt.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ def encode(payload)
::JWT.encode(merge_claims(payload),
@options[:key], @options[:algoritm])
end

def decode_and_verify(token, verify_options)
@verify_options = verify_options.reverse_merge({
verify_expiration: true,
Expand Down
Loading

0 comments on commit 873bf70

Please sign in to comment.