-
Notifications
You must be signed in to change notification settings - Fork 5
Expand file tree
/
Copy pathapp.rb
More file actions
175 lines (152 loc) · 4.7 KB
/
app.rb
File metadata and controls
175 lines (152 loc) · 4.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
# frozen_string_literal: true
require 'bundler/setup'
require 'base64'
require 'cgi'
require 'digest/sha2'
require 'erb'
require 'jwt'
require 'oauth2'
require 'pco_api'
require 'securerandom'
require 'sinatra/base'
require 'sinatra/reloader'
require 'time'
class ExampleApp < Sinatra::Base
OAUTH_APP_ID = ENV.fetch('OAUTH_APP_ID').freeze
OAUTH_SECRET = ENV.fetch('OAUTH_SECRET').freeze
PUBLIC_OAUTH_APP_ID = ENV.fetch('PUBLIC_OAUTH_APP_ID').freeze
SCOPE = ENV.fetch('SCOPE', 'openid people services').freeze
DOMAIN = ENV.fetch('DOMAIN', 'http://localhost:4567').freeze
API_URL = ENV.fetch('API_URL', 'https://api.planningcenteronline.com').freeze
TOKEN_EXPIRATION_PADDING = 300 # go ahead and refresh a token if it's within this many seconds of expiring
enable :sessions
set :session_secret, ENV.fetch('SESSION_SECRET')
configure :development do
register Sinatra::Reloader
end
helpers do
def h(html)
CGI.escapeHTML html
end
end
def client
OAuth2::Client.new(OAUTH_APP_ID, OAUTH_SECRET, site: API_URL)
end
def token
return if session[:token].nil?
token = OAuth2::AccessToken.from_hash(client, session[:token].dup)
if token.expires? && (token.expires_at < Time.now.to_i + TOKEN_EXPIRATION_PADDING) && token.refresh_token
# looks like our token will expire soon and we have a refresh token,
# so let's get a new access token
token = token.refresh!
session[:token] = token.to_hash
end
token
rescue OAuth2::Error
# our token info is bad, let's start over
session[:token] = nil
end
def api
PCO::API.new(oauth_access_token: token.token, url: API_URL)
end
def fetch_user_info
# if the id_token is not present then no openid scopes were requested
return unless token && token.params['id_token']
userinfo_response = token.get('/oauth/userinfo')
JSON.parse(userinfo_response.body)
end
def parse_id_token(id_token)
return unless id_token
# Basic JWT decoding without signature verification for demo purposes only.
#
# NOTE: In production, you should verify the JWT signature using the JWK from the
# JWKS endpoint ({API_URL}/oauth/discovery/keys).
#
# This verification will happen automatically if using an authentication gem
# like `omniauth` with the `omniauth_openid_connect` strategy. Otherwise a gem
# like `json-jwt` can be used to manually build the public key from the JWK hash to
# pass into `JWT.decode`.
JWT.decode(id_token, nil, false).first
rescue JWT::DecodeError
nil
end
get '/' do
if token
redirect '/people'
else
erb :login
end
end
get '/people' do
if token
@logged_in = true
begin
response = api.people.v2.people.get
rescue PCO::API::Errors::Unauthorized
# token probably revoked
session[:token] = nil
redirect '/'
else
@people = response['data']
@formatted_response = JSON.pretty_generate(response)
erb :people
end
else
redirect '/'
end
end
get '/auth' do
session[:code_verifier] = SecureRandom.urlsafe_base64(48)
code_challenge = Base64.urlsafe_encode64(Digest::SHA256.digest(session[:code_verifier]), padding: false)
# redirect the user to PCO where they can authorize our app
url = client.auth_code.authorize_url(
scope: SCOPE,
prompt: 'select_account', # to allow user account selection or "login" to force re-authentication
redirect_uri: "#{DOMAIN}/auth/complete",
code_challenge: code_challenge,
code_challenge_method: "S256"
)
redirect url
end
get '/auth/complete' do
# user was redirected back after they authorized our app
token = client.auth_code.get_token(
params[:code],
redirect_uri: "#{DOMAIN}/auth/complete",
code_verifier: session[:code_verifier]
)
# store the auth token, id token, and refresh token info in our session
session[:token] = token.to_hash
if (id_token = token.params['id_token'])
session[:current_user] = {
name: fetch_user_info&.dig('name'),
claims: parse_id_token(id_token)
}
end
redirect '/'
end
get '/profile' do
if token
@userinfo_claims = fetch_user_info
@id_token_claims = session.dig(:current_user, :claims)
@logged_in = true
erb :profile
else
redirect '/'
end
end
get '/auth/logout' do
# make an api call to PCO to revoke the access token
api.oauth.revoke.post(
token: token.token,
client_id: OAUTH_APP_ID,
client_secret: OAUTH_SECRET
)
rescue PCO::API::Errors::Forbidden
# noop - token already revoked
ensure
session.clear
redirect '/'
end
run! if app_file == $PROGRAM_NAME
end