keratin-authn 0.1.3 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +47 -23
- data/bin/console +1 -1
- data/lib/keratin/authn.rb +46 -38
- data/lib/keratin/authn/issuer.rb +14 -10
- data/lib/keratin/authn/test/helpers.rb +1 -1
- data/lib/keratin/authn/version.rb +1 -1
- data/lib/keratin/client.rb +78 -0
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5bc3dc3a5993e496aa7d4a27460d799b4e3ea600
|
4
|
+
data.tar.gz: 30266ffdffaa188785ce930f0f470a5cde684746
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 77ac60816689bd685f7ba0e6de2122bcd6e372bccee8b6bb1e1ef6bf660278fd3617ca7e5fe57debbe792db44baa030fc55d721681b535bb2e0de6ad435604ad
|
7
|
+
data.tar.gz: 0814a5099a9b4d7c246c9a9b3dabe05e8d234c6fc86f6022cfd3ba54a3f76df521dad46a1130ef4e422a1e1125c523543d2d22efca6d6067dba15f39589db889
|
data/README.md
CHANGED
@@ -25,53 +25,77 @@ Keratin::AuthN.config.tap do |config|
|
|
25
25
|
|
26
26
|
# The domain of your application
|
27
27
|
config.audience = 'myapp.com'
|
28
|
+
|
29
|
+
# HTTP basic auth for using AuthN's private endpoints
|
30
|
+
config.username = 'secret'
|
31
|
+
config.password = 'secret'
|
28
32
|
end
|
29
33
|
```
|
30
34
|
|
31
|
-
|
35
|
+
### Reading the Session
|
32
36
|
|
33
|
-
|
37
|
+
Use `Keratin::AuthN.subject_from(params[:authn])` to fetch an `account_id` from the session if and
|
38
|
+
only if the session is valid.
|
34
39
|
|
35
|
-
###
|
40
|
+
### Logging Out
|
41
|
+
|
42
|
+
Send users to `Keratin::AuthN.logout_url(return_to: some_path)` to log them out from the AuthN
|
43
|
+
server. If you use [keratin/authn-js](https://github.com/keratin/authn-js), you might prefer the
|
44
|
+
logout functionality there as it can also take care of deleting the cookie.
|
45
|
+
|
46
|
+
### Modifying Accounts
|
47
|
+
|
48
|
+
* `Keratin.authn.lock(account_id)`: will lock an account, revoking all sessions (when they time out)
|
49
|
+
and disallowing any new logins. Intended for user moderation actions.
|
50
|
+
* `Keratin.authn.unlock(account_id)`: will unlock an account, restoring normal functionality.
|
51
|
+
* `Keratin.authn.archive(account_id)`: will wipe all personal information, including username and
|
52
|
+
password. Intended for user deletion routine.
|
53
|
+
|
54
|
+
### Example: Sessions
|
55
|
+
|
56
|
+
You should store the token in a cookie (the [keratin/authn-js](https://github.com/keratin/authn-js)
|
57
|
+
integration can do this automatically) and continue using it to verify a logged-in session:
|
36
58
|
|
37
59
|
```ruby
|
38
|
-
class
|
39
|
-
|
40
|
-
@user = User.new(params.require(:user).permit(:name, :email))
|
41
|
-
@user.account_id = Keratin::AuthN.subject_from(params[:user][:id_token])
|
60
|
+
class ApplicationController
|
61
|
+
private
|
42
62
|
|
43
|
-
|
63
|
+
def logged_in?
|
64
|
+
!! current_account_id
|
65
|
+
end
|
66
|
+
|
67
|
+
def current_user
|
68
|
+
return @current_user if defined? @current_user
|
69
|
+
@current_user = User.find_by_account_id(current_account_id)
|
70
|
+
end
|
71
|
+
|
72
|
+
def current_account_id
|
73
|
+
Keratin::AuthN.subject_from(cookies[:authn])
|
44
74
|
end
|
45
75
|
end
|
46
76
|
```
|
47
77
|
|
48
|
-
### Example:
|
78
|
+
### Example: Signup
|
49
79
|
|
50
80
|
```ruby
|
51
|
-
class
|
81
|
+
class UsersController
|
52
82
|
def create
|
53
|
-
@user = User.
|
83
|
+
@user = User.new(params.require(:user).permit(:name, :email))
|
84
|
+
@user.account_id = current_account_id
|
54
85
|
|
55
86
|
# ...
|
56
87
|
end
|
57
88
|
end
|
58
89
|
```
|
59
90
|
|
60
|
-
### Example:
|
61
|
-
|
62
|
-
You should store the token in a cookie and continue using it to verify a logged-in session:
|
91
|
+
### Example: Login
|
63
92
|
|
64
93
|
```ruby
|
65
|
-
class
|
66
|
-
|
67
|
-
|
68
|
-
def logged_in?
|
69
|
-
!! Keratin::AuthN.subject_from(cookies[:id_token])
|
70
|
-
end
|
94
|
+
class SessionsController
|
95
|
+
def create
|
96
|
+
@user = current_user
|
71
97
|
|
72
|
-
|
73
|
-
return @current_user if defined? @current_user
|
74
|
-
@current_user = User.find_by_account_id(Keratin::AuthN.subject_from(cookies[:id_token])
|
98
|
+
# ...
|
75
99
|
end
|
76
100
|
end
|
77
101
|
```
|
data/bin/console
CHANGED
data/lib/keratin/authn.rb
CHANGED
@@ -6,52 +6,60 @@ require_relative 'authn/issuer'
|
|
6
6
|
require 'lru_redux'
|
7
7
|
require 'json/jwt'
|
8
8
|
|
9
|
-
module Keratin
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
# the base url of the service handling authentication
|
16
|
-
# e.g. "https://issuer.tech"
|
17
|
-
attr_accessor :issuer
|
18
|
-
|
19
|
-
# the path where we can fetch configuration from our issuer.
|
20
|
-
#
|
21
|
-
# default: "/configuration"
|
22
|
-
attr_accessor :configuration_path
|
23
|
-
|
24
|
-
# how long (in seconds) to keep keys in the keychain before refreshing.
|
25
|
-
# default: 3600
|
26
|
-
attr_accessor :keychain_ttl
|
9
|
+
module Keratin
|
10
|
+
def self.authn
|
11
|
+
@authn ||= AuthN::Issuer.new(AuthN.config.issuer,
|
12
|
+
username: AuthN.config.username,
|
13
|
+
password: AuthN.config.password
|
14
|
+
)
|
27
15
|
end
|
28
16
|
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
17
|
+
module AuthN
|
18
|
+
class Config
|
19
|
+
# the domain (host) of the main application.
|
20
|
+
# e.g. "audience.tech"
|
21
|
+
attr_accessor :audience
|
22
|
+
|
23
|
+
# the base url of the service handling authentication
|
24
|
+
# e.g. "https://issuer.tech"
|
25
|
+
attr_accessor :issuer
|
26
|
+
|
27
|
+
# how long (in seconds) to keep keys in the keychain before refreshing.
|
28
|
+
# default: 3600
|
29
|
+
attr_accessor :keychain_ttl
|
30
|
+
|
31
|
+
# the http basic auth username for accessing private endpoints of the authn issuer.
|
32
|
+
attr_accessor :username
|
33
|
+
|
34
|
+
# the http basic auth password for accessing private endpoints of the authn issuer.
|
35
|
+
attr_accessor :password
|
33
36
|
end
|
34
|
-
end
|
35
37
|
|
36
|
-
|
37
|
-
|
38
|
-
|
38
|
+
def self.config
|
39
|
+
@config ||= Config.new.tap do |config|
|
40
|
+
config.keychain_ttl = 3600
|
41
|
+
end
|
42
|
+
end
|
39
43
|
|
40
|
-
|
41
|
-
|
42
|
-
# verifying the signature.
|
43
|
-
#
|
44
|
-
# this may involve HTTP requests to fetch the issuer's configuration and JWKs.
|
45
|
-
def subject_from(id_token)
|
46
|
-
verifier = IDTokenVerifier.new(id_token, keychain)
|
47
|
-
verifier.subject if verifier.verified?
|
44
|
+
def self.keychain
|
45
|
+
@keychain ||= LruRedux::TTL::ThreadSafeCache.new(25, config.keychain_ttl)
|
48
46
|
end
|
49
47
|
|
50
|
-
|
51
|
-
|
48
|
+
class << self
|
49
|
+
# safely fetches a subject from the id token after checking relevant claims and
|
50
|
+
# verifying the signature.
|
51
|
+
#
|
52
|
+
# this may involve HTTP requests to fetch the issuer's configuration and JWKs.
|
53
|
+
def subject_from(id_token)
|
54
|
+
verifier = IDTokenVerifier.new(id_token, keychain)
|
55
|
+
verifier.subject if verifier.verified?
|
56
|
+
end
|
52
57
|
|
53
|
-
|
58
|
+
def logout_url(return_to: nil)
|
59
|
+
query = {redirect_uri: return_to}.to_param if return_to
|
60
|
+
|
61
|
+
"#{config.issuer}/sessions/logout#{?? if query}#{query}"
|
62
|
+
end
|
54
63
|
end
|
55
64
|
end
|
56
|
-
|
57
65
|
end
|
data/lib/keratin/authn/issuer.rb
CHANGED
@@ -1,10 +1,18 @@
|
|
1
|
+
require 'keratin/client'
|
1
2
|
require 'net/http'
|
2
3
|
|
3
4
|
module Keratin::AuthN
|
4
|
-
class Issuer
|
5
|
-
def
|
6
|
-
|
7
|
-
|
5
|
+
class Issuer < Keratin::Client
|
6
|
+
def lock(account_id)
|
7
|
+
patch(path: "/accounts/:account_id/lock").result
|
8
|
+
end
|
9
|
+
|
10
|
+
def unlock(account_id)
|
11
|
+
patch(path: "/accounts/:account_id/unlock").result
|
12
|
+
end
|
13
|
+
|
14
|
+
def archive(account_id)
|
15
|
+
delete(path: "/accounts/:account_id").result
|
8
16
|
end
|
9
17
|
|
10
18
|
def signing_key
|
@@ -12,16 +20,12 @@ module Keratin::AuthN
|
|
12
20
|
end
|
13
21
|
|
14
22
|
def configuration
|
15
|
-
@configuration ||=
|
16
|
-
Net::HTTP.get(URI.parse(@config_uri))
|
17
|
-
)
|
23
|
+
@configuration ||= get(path: '/configuration').data
|
18
24
|
end
|
19
25
|
|
20
26
|
def keys
|
21
27
|
@keys ||= JSON::JWK::Set.new(
|
22
|
-
|
23
|
-
Net::HTTP.get(URI.parse(configuration['jwks_uri']))
|
24
|
-
)
|
28
|
+
get(path: URI.parse(configuration['jwks_uri']).path).data
|
25
29
|
)
|
26
30
|
end
|
27
31
|
end
|
@@ -26,7 +26,7 @@ module Keratin::AuthN
|
|
26
26
|
# stubs the endpoints necessary to validate a signed JWT
|
27
27
|
private def stub_auth_server(issuer: Keratin::AuthN.config.issuer, keypair: jws_keypair)
|
28
28
|
Keratin::AuthN.keychain.clear
|
29
|
-
stub_request(:get, "#{issuer}
|
29
|
+
stub_request(:get, "#{issuer}/configuration").to_return(
|
30
30
|
status: 200,
|
31
31
|
body: {'jwks_uri' => "#{issuer}/jwks"}.to_json
|
32
32
|
)
|
@@ -0,0 +1,78 @@
|
|
1
|
+
module Keratin
|
2
|
+
class Error < StandardError; end
|
3
|
+
|
4
|
+
class ServiceResult
|
5
|
+
attr_reader :data
|
6
|
+
attr_reader :result
|
7
|
+
|
8
|
+
def initialize(data)
|
9
|
+
@data = data
|
10
|
+
@result = data['result']
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
class ClientError < Keratin::Error
|
15
|
+
attr_reader :errors
|
16
|
+
|
17
|
+
def initialize(errors)
|
18
|
+
@errors = errors.map{|e| [e['field'], e['message']] }
|
19
|
+
.group_by(&:first)
|
20
|
+
.map{|k, v| [k, v.map(&:last)] }
|
21
|
+
.to_h
|
22
|
+
|
23
|
+
super(@errors.inspect)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
class ServiceError < Keratin::Error
|
28
|
+
end
|
29
|
+
|
30
|
+
class Client
|
31
|
+
|
32
|
+
attr_reader :base
|
33
|
+
|
34
|
+
def initialize(base_url, username: nil, password: nil)
|
35
|
+
@base = base_url.chomp('/')
|
36
|
+
@auth = [username, password] if username && password
|
37
|
+
end
|
38
|
+
|
39
|
+
private def get(**opts)
|
40
|
+
submit(Net::HTTP::Get, **opts)
|
41
|
+
end
|
42
|
+
|
43
|
+
private def patch(**opts)
|
44
|
+
submit(Net::HTTP::Patch, **opts)
|
45
|
+
end
|
46
|
+
|
47
|
+
private def delete(**opts)
|
48
|
+
submit(Net::HTTP::Delete, **opts)
|
49
|
+
end
|
50
|
+
|
51
|
+
private def submit(request_klass, path:)
|
52
|
+
uri = URI.parse("#{base}#{path}")
|
53
|
+
|
54
|
+
request = request_klass.new(uri)
|
55
|
+
request.basic_auth(*@auth) if @auth
|
56
|
+
|
57
|
+
Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https') do |http|
|
58
|
+
http.open_timeout = 0.5
|
59
|
+
http.read_timeout = 2.0
|
60
|
+
|
61
|
+
case response = http.request(request)
|
62
|
+
when Net::HTTPSuccess
|
63
|
+
return ServiceResult.new(JSON.parse(response.body))
|
64
|
+
when Net::HTTPRedirection
|
65
|
+
return ServiceResult.new('result' => {
|
66
|
+
'location' => response['Location']
|
67
|
+
})
|
68
|
+
when Net::HTTPClientError
|
69
|
+
raise ClientError.new(JSON.parse(response.body)['errors'])
|
70
|
+
when Net::HTTPServerError
|
71
|
+
raise ServiceError.new(response.body)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
rescue Net::OpenTimeout, Net::ReadTimeout => e
|
75
|
+
raise ServiceError.new(e.message)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: keratin-authn
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Lance Ivy
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-
|
11
|
+
date: 2016-12-10 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: json-jwt
|
@@ -132,6 +132,7 @@ files:
|
|
132
132
|
- lib/keratin/authn/test/helpers.rb
|
133
133
|
- lib/keratin/authn/testing.rb
|
134
134
|
- lib/keratin/authn/version.rb
|
135
|
+
- lib/keratin/client.rb
|
135
136
|
homepage:
|
136
137
|
licenses:
|
137
138
|
- LGPL-3.0
|