Skip to content

Commit

Permalink
Use PKCE flow with public and confidential apps
Browse files Browse the repository at this point in the history
  • Loading branch information
felipeelias committed May 5, 2019
1 parent 6980294 commit e4b5831
Show file tree
Hide file tree
Showing 8 changed files with 127 additions and 26 deletions.
6 changes: 6 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
.env
.semaphore-cache
.vscode
.rspec
log
tmp
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1 @@
env.rb
.env
14 changes: 14 additions & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
AllCops:
TargetRubyVersion: 2.6

Metrics/BlockLength:
Enabled: false

Metrics/ClassLength:
Enabled: false

Metrics/LineLength:
Max: 120

Style/Documentation:
Enabled: false
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ source 'https://rubygems.org'

ruby '2.6.3'

gem 'dotenv'
gem 'oauth2'
gem 'puma'
gem 'redcarpet'
Expand Down
2 changes: 2 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ GEM
specs:
ast (2.4.0)
coderay (1.1.2)
dotenv (2.7.2)
faraday (0.15.4)
multipart-post (>= 1.2, < 3)
jaro_winkler (1.5.2)
Expand Down Expand Up @@ -50,6 +51,7 @@ PLATFORMS
ruby

DEPENDENCIES
dotenv
oauth2
pry
puma
Expand Down
2 changes: 2 additions & 0 deletions config/puma.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# frozen_string_literal: true

workers Integer(ENV['WEB_CONCURRENCY'] || 2)
threads_count = Integer(ENV['SINATRA_MAX_THREADS'] || 5)
threads threads_count, threads_count
Expand Down
123 changes: 98 additions & 25 deletions doorkeeper_client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,34 @@

require 'sinatra/base'
require 'securerandom'
require 'singleton'
require 'dotenv/load'
require './lib/html_renderer'

# Load custom environment variables
load 'env.rb' if File.exist?('env.rb')

Rollbar.configure do |config|
config.access_token = ENV['ROLLBAR_ACCESS_TOKEN']
end

class App
include Singleton

attr_accessor :public_client_id,
:public_client_redirect_uri,
:confidential_client_id,
:confidential_client_secret,
:confidential_client_redirect_uri,
:provider_url
end

App.instance.tap do |app|
app.public_client_id = ENV['PUBLIC_CLIENT_ID']
app.public_client_redirect_uri = ENV['PUBLIC_CLIENT_REDIRECT_URI']
app.confidential_client_id = ENV['CONFIDENTIAL_CLIENT_ID']
app.confidential_client_secret = ENV['CONFIDENTIAL_CLIENT_SECRET']
app.confidential_client_redirect_uri = ENV['CONFIDENTIAL_CLIENT_REDIRECT_URI']
app.provider_url = ENV['PROVIDER_URL']
end

class DoorkeeperClient < Sinatra::Base
require 'rollbar/middleware/sinatra'
use Rollbar::Middleware::Sinatra
Expand All @@ -29,14 +48,16 @@ def signed_in?
!session[:access_token].nil?
end

def state_matches?
return false if blank?(params[:state])
return false if blank?(session[:state])
params[:state] == session[:state]
def state_matches?(prev_state, new_state)
return false if blank?(prev_state)
return false if blank?(new_state)

prev_state == new_state
end

def blank?(string)
return true if string.nil?

/\A[[:space:]]*\z/.match?(string.to_s)
end

Expand All @@ -51,52 +72,104 @@ def markdown_readme
end

def site_host
URI.parse(ENV['SITE']).host
URI.parse(app.provider_url).host
end
end

def client(token_method = :post)
OAuth2::Client.new(
ENV['OAUTH2_CLIENT_ID'],
ENV['OAUTH2_CLIENT_SECRET'],
site: ENV['SITE'] || 'http://doorkeeper-provider.herokuapp.com',
token_method: token_method
)
def app
App.instance
end

def client
public_send("#{session[:client]}_client")
end

def public_client
OAuth2::Client.new(app.public_client_id, nil, site: app.provider_url)
end

def confidential_client
OAuth2::Client.new(app.confidential_client_id, app.confidential_client_secret, site: app.provider_url)
end

def access_token
OAuth2::AccessToken.new(client, session[:access_token], refresh_token: session[:refresh_token])
end

def redirect_uri
ENV['OAUTH2_CLIENT_REDIRECT_URI']
def generate_state!
session[:state] = SecureRandom.hex
end

def generate_code_verifier!
session[:code_verifier] = SecureRandom.uuid
end

def state
session[:state]
end

def code_verifier
session[:code_verifier]
end

def code_challenge_method
'S256'
end

def code_challenge
Base64.urlsafe_encode64(Digest::SHA256.digest(session[:code_verifier])).split('=').first
end

def authorize_url_for_client(type)
session[:client] = type

client.auth_code.authorize_url(
redirect_uri: app.confidential_client_redirect_uri,
scope: 'read',
state: generate_state!,
code_challenge_method: code_challenge_method,
code_challenge: code_challenge
)
end

get '/' do
erb :home
end

get '/sign_in' do
session[:state] = SecureRandom.hex
scope = params[:scope] || 'read'
redirect client.auth_code.authorize_url(redirect_uri: redirect_uri, scope: scope, state: session[:state])
generate_code_verifier!
redirect authorize_url_for_client(:confidential)
end

get '/public_sign_in' do
generate_code_verifier!
redirect authorize_url_for_client(:public)
end

get '/sign_out' do
session[:access_token] = nil
session[:refresh_token] = nil
redirect '/'
end

get '/callback' do
if params[:error]
erb :callback_error, layout: !request.xhr?
else
if !state_matches?
unless state_matches?(state, params[:state])
redirect '/'
return
end

new_token = client.auth_code.get_token(params[:code], redirect_uri: redirect_uri)
new_token =
client
.auth_code
.get_token(
params[:code],
redirect_uri: app.confidential_client_redirect_uri,
code_verifier: code_verifier
)

session[:access_token] = new_token.token
session[:refresh_token] = new_token.refresh_token
redirect '/'
Expand All @@ -108,9 +181,9 @@ def redirect_uri
session[:access_token] = new_token.token
session[:refresh_token] = new_token.refresh_token
redirect '/'
rescue OAuth2::Error => @error
rescue OAuth2::Error => _e
erb :error, layout: !request.xhr?
rescue StandardError => @error
rescue StandardError => _e
erb :error, layout: !request.xhr?
end

Expand All @@ -121,7 +194,7 @@ def redirect_uri
response = access_token.get("/api/v1/#{params[:api]}")
@json = JSON.parse(response.body)
erb :explore, layout: !request.xhr?
rescue OAuth2::Error => @error
rescue OAuth2::Error => _e
erb :error, layout: !request.xhr?
end
end
Expand Down
3 changes: 3 additions & 0 deletions views/home.erb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@
<a href="/sign_in" class="btn btn-success" role="button">
Sign in on <%= site_host %>
</a>
<a href="/public_sign_in" class="btn btn-success" role="button">
Sign in with the public client on <%= site_host %>
</a>
<% end %>
</p>
<% else %>
Expand Down

0 comments on commit e4b5831

Please sign in to comment.