cf-uaa-lib 3.14.1 → 4.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.github/dependabot.yml +11 -0
- data/.github/workflows/gem-push.yml +29 -0
- data/.github/workflows/ruby.yml +27 -0
- data/README.md +116 -14
- data/cf-uaa-lib.gemspec +9 -9
- data/examples/password_grant_and_decode_token.rb +53 -0
- data/lib/uaa/http.rb +2 -0
- data/lib/uaa/info.rb +2 -10
- data/lib/uaa/scim.rb +11 -11
- data/lib/uaa/token_coder.rb +25 -8
- data/lib/uaa/token_issuer.rb +20 -13
- data/lib/uaa/util.rb +1 -1
- data/lib/uaa/version.rb +1 -1
- data/spec/info_spec.rb +3 -3
- data/spec/integration_spec.rb +19 -19
- data/spec/scim_spec.rb +11 -11
- data/spec/spec_helper.rb +7 -1
- data/spec/token_coder_spec.rb +20 -14
- data/spec/token_issuer_spec.rb +57 -25
- metadata +75 -46
- data/.travis.yml +0 -10
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 283326148ca7b9df992d6ea50b3e0161ef74d07626147979c83a5c6e63daec86
|
4
|
+
data.tar.gz: b643f26e60fa73ccdee7e9fc7b89b3b3c010e9c9007be12e98e2f7493cc53a6d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 110d6d2259dcce0a7e1cac4ffbf24625a32c6a265fc5329adf4d2cc3ade2803aed54012021d8ad2238cd0aeb5259d3b973173671c7ca1b54fe6cc775146a5001
|
7
|
+
data.tar.gz: d217c50866d14c2eac62b03dbe406f572398f9f1d747b0a079c26236662474143d6d252b609df03c228123c677b299d1f3b8e700f49af7f0651561d3e715f647
|
@@ -0,0 +1,29 @@
|
|
1
|
+
name: Ruby Gem
|
2
|
+
|
3
|
+
on: workflow_dispatch
|
4
|
+
|
5
|
+
jobs:
|
6
|
+
build:
|
7
|
+
name: Build + Publish
|
8
|
+
runs-on: ubuntu-latest
|
9
|
+
permissions:
|
10
|
+
contents: read
|
11
|
+
packages: write
|
12
|
+
|
13
|
+
steps:
|
14
|
+
- uses: actions/checkout@v2
|
15
|
+
- name: Set up Ruby 2.6
|
16
|
+
uses: actions/setup-ruby@v1
|
17
|
+
with:
|
18
|
+
ruby-version: 2.6.x
|
19
|
+
|
20
|
+
- name: Publish to RubyGems
|
21
|
+
run: |
|
22
|
+
mkdir -p $HOME/.gem
|
23
|
+
touch $HOME/.gem/credentials
|
24
|
+
chmod 0600 $HOME/.gem/credentials
|
25
|
+
printf -- "---\n:rubygems_api_key: ${GEM_HOST_API_KEY}\n" > $HOME/.gem/credentials
|
26
|
+
gem build *.gemspec
|
27
|
+
gem push *.gem
|
28
|
+
env:
|
29
|
+
GEM_HOST_API_KEY: "${{ secrets.RUBYGEMS_AUTH_TOKEN }}"
|
@@ -0,0 +1,27 @@
|
|
1
|
+
name: Ruby
|
2
|
+
|
3
|
+
on:
|
4
|
+
push:
|
5
|
+
branches: [ master ]
|
6
|
+
pull_request:
|
7
|
+
branches: [ master ]
|
8
|
+
|
9
|
+
jobs:
|
10
|
+
test:
|
11
|
+
|
12
|
+
runs-on: ubuntu-latest
|
13
|
+
strategy:
|
14
|
+
matrix:
|
15
|
+
ruby-version: ['2.5', '2.7', '3.0', '3.1']
|
16
|
+
|
17
|
+
steps:
|
18
|
+
- uses: actions/checkout@v2
|
19
|
+
- name: Set up Ruby
|
20
|
+
uses: ruby/setup-ruby@v1
|
21
|
+
with:
|
22
|
+
ruby-version: ${{ matrix.ruby-version }}
|
23
|
+
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
|
24
|
+
- name: Run tests
|
25
|
+
run: bundle exec rake
|
26
|
+
- name: Run coverage
|
27
|
+
run: bundle exec rake cov
|
data/README.md
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
# CloudFoundry UAA Gem
|
2
|
-
|
2
|
+
![Build status](https://github.com/cloudfoundry/cf-uaa-lib/actions/workflows/ruby.yml/badge.svg?branch=master)
|
3
3
|
[![Gem Version](https://badge.fury.io/rb/cf-uaa-lib.png)](http://badge.fury.io/rb/cf-uaa-lib)
|
4
4
|
|
5
5
|
Client gem for interacting with the [CloudFoundry UAA server](https://github.com/cloudfoundry/uaa)
|
@@ -8,31 +8,133 @@ For documentation see: https://rubygems.org/gems/cf-uaa-lib
|
|
8
8
|
|
9
9
|
## Install from rubygems
|
10
10
|
|
11
|
-
|
11
|
+
```plain
|
12
|
+
gem install cf-uaa-lib
|
13
|
+
```
|
12
14
|
|
13
15
|
## Build from source
|
14
16
|
|
15
|
-
|
16
|
-
|
17
|
-
|
17
|
+
```plain
|
18
|
+
bundle install
|
19
|
+
rake install
|
20
|
+
```
|
18
21
|
|
19
22
|
## Use the gem
|
20
23
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
24
|
+
Create a UAA client that allows users to authenticate with username/password and allow client application to use `openid` scope to invoke `/userinfo` endpoint for the user.
|
25
|
+
|
26
|
+
```plain
|
27
|
+
uaa create-client decode-token-demo -s decode-token-demo -v \
|
28
|
+
--authorized_grant_types password,refresh_token \
|
29
|
+
--scope "openid" \
|
30
|
+
--authorities uaa.none
|
31
|
+
```
|
32
|
+
|
33
|
+
Create a user with which to authorize our `decode-token-demo` client application.
|
34
|
+
|
35
|
+
```plain
|
36
|
+
uaa create-user myuser \
|
37
|
+
--email myuser@example.com \
|
38
|
+
--givenName "My" \
|
39
|
+
--familyName "User" \
|
40
|
+
--password myuser_secret
|
41
|
+
```
|
42
|
+
|
43
|
+
Create this Ruby script (script is available at `examples/password_grant_and_decode_token.rb`):
|
44
|
+
|
45
|
+
```ruby
|
46
|
+
#!/usr/bin/env ruby
|
47
|
+
|
48
|
+
require 'uaa'
|
49
|
+
|
50
|
+
url = ENV["UAA_URL"]
|
51
|
+
client, secret = "decode-token-demo", "decode-token-demo"
|
52
|
+
username, password = ENV["UAA_USERNAME"], ENV["UAA_PASSWORD"]
|
53
|
+
|
54
|
+
def show(title, object)
|
55
|
+
puts "#{title}: #{object.inspect}"
|
56
|
+
puts
|
57
|
+
end
|
58
|
+
|
59
|
+
uaa_options = {}
|
60
|
+
uaa_options[:ssl_ca_file] = ENV["UAA_CA_CERT_FILE"] if ENV["UAA_CA_CERT_FILE"]
|
61
|
+
show "uaa_options", uaa_options
|
62
|
+
|
63
|
+
uaa_info = CF::UAA::Info.new(url, uaa_options)
|
64
|
+
show "UAA server info", uaa_info.server
|
65
|
+
|
66
|
+
token_keys = uaa_info.validation_keys_hash
|
67
|
+
show "Signing keys for access tokens", token_keys
|
68
|
+
|
69
|
+
token_issuer = CF::UAA::TokenIssuer.new(url, client, secret, uaa_options)
|
70
|
+
show "Login prompts", token_issuer.prompts
|
71
|
+
|
72
|
+
token = token_issuer.owner_password_grant(username, password, "openid")
|
73
|
+
show "User '#{username}' password grant", token
|
74
|
+
|
75
|
+
auth_header = "bearer #{token.info["access_token"]}"
|
76
|
+
show "Auth header for resource server API calls", auth_header
|
77
|
+
|
78
|
+
userinfo = uaa_info.whoami(auth_header)
|
79
|
+
show "User info", userinfo
|
80
|
+
|
81
|
+
last_exception = nil
|
82
|
+
token_keys.each_pair do |keyname, token_key|
|
83
|
+
begin
|
84
|
+
token_coder = CF::UAA::TokenCoder.new(uaa_options.merge(pkey: token_key["value"], verify: true))
|
85
|
+
token_info = token_coder.decode(auth_header)
|
86
|
+
show "Decoded access token", token_info
|
87
|
+
last_exception = nil
|
88
|
+
rescue CF::UAA::Decode => e
|
89
|
+
last_exception = e
|
90
|
+
end
|
91
|
+
end
|
92
|
+
raise last_exception if last_exception
|
93
|
+
```
|
94
|
+
|
95
|
+
To run the script, setup the env vars for your UAA and run the ruby script:
|
96
|
+
|
97
|
+
```bash
|
98
|
+
export UAA_URL=https://192.168.50.6:8443
|
99
|
+
export UAA_CA_CERT_FILE=/path/to/ca.pem
|
100
|
+
export UAA_USERNAME=myuser
|
101
|
+
export UAA_PASSWORD=myuser_secret
|
102
|
+
ruby examples/password_grant_and_decode_token.rb
|
103
|
+
```
|
104
|
+
|
105
|
+
The output will look similar to:
|
106
|
+
|
107
|
+
```plain
|
108
|
+
uaa_options: {:ssl_ca_file=>"/var/folders/wd/xnncwqp96rj0v1y2nms64mq80000gn/T/tmp.R6wpXYdC/ca.pem"}
|
109
|
+
|
110
|
+
UAA server info: {"app"=>{"version"=>"4.19.0"}, "links"=>{"uaa"=>"https://192.168.50.6:8443", "passwd"=>"/forgot_password", "login"=>"https://192.168.50.6:8443", "register"=>"/create_account"}, "zone_name"=>"uaa", "entityID"=>"192.168.50.6:8443", "commit_id"=>"7897100", "idpDefinitions"=>{}, "prompts"=>{"username"=>["text", "Email"], "password"=>["password", "Password"]}, "timestamp"=>"2018-06-13T12:02:09-0700"}
|
111
|
+
|
112
|
+
Cookie#domain returns dot-less domain name now. Use Cookie#dot_domain if you need "." at the beginning.
|
113
|
+
Signing keys for access tokens: {"uaa-jwt-key-1"=>{"kty"=>"RSA", "e"=>"AQAB", "use"=>"sig", "kid"=>"uaa-jwt-key-1", "alg"=>"RS256", "value"=>"-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA8UNioYHjhyi1qSHrnBZ9\nKE96/1jLBOX2UTShGBo8jP7eDD6zUh5DNHNPAwD1V8gNI4wvNAm+zL1MrSEDWzn2\nPvCANd+XydoNVZU1zhqxvGhoxHmgAA3JbgSS3oLLNDG/HH8wEnjAxb+G1uh2EVSF\nAe/euQ/fEmY4e7uOG34h9WMX84tD1Sf/xvVoNGAL8bTwotzBLFZ12M3P70hrKDi5\n9wEBbY5bllvvNFyjZTYwMbw97RIOdg3FQkOABu8ENCqbPks5gqSpNV33ekaX4rAd\nwYdEX5iUzDBdMyD8jqUopuqTXqBKg2/ealGitXdbSIEAvcBgZWnn1j2vFp6OEYBB\n7wIDAQAB\n-----END PUBLIC KEY-----", "n"=>"APFDYqGB44cotakh65wWfShPev9YywTl9lE0oRgaPIz-3gw-s1IeQzRzTwMA9VfIDSOMLzQJvsy9TK0hA1s59j7wgDXfl8naDVWVNc4asbxoaMR5oAANyW4Ekt6CyzQxvxx_MBJ4wMW_htbodhFUhQHv3rkP3xJmOHu7jht-IfVjF_OLQ9Un_8b1aDRgC_G08KLcwSxWddjNz-9Iayg4ufcBAW2OW5Zb7zRco2U2MDG8Pe0SDnYNxUJDgAbvBDQqmz5LOYKkqTVd93pGl-KwHcGHRF-YlMwwXTMg_I6lKKbqk16gSoNv3mpRorV3W0iBAL3AYGVp59Y9rxaejhGAQe8"}}
|
114
|
+
|
115
|
+
Login prompts: {"username"=>["text", "Email"], "password"=>["password", "Password"]}
|
116
|
+
|
117
|
+
User 'myuser' password grant: #<CF::UAA::TokenInfo:0x00007fbad5a12c18 @info={"access_token"=>"eyJhbGciOiJSUzI1NiIsImtpZCI6InVhYS1qd3Qta2V5LTEiLCJ0eXAiOiJKV1QifQ.eyJqdGkiOiJlMTFlZmMwNjI1OGQ0MzA0YTc4ZGIyNzliYjJjMzQ1OCIsInN1YiI6IjM5NzhmZjRkLWQ3MzgtNGI4Yi05OTA4LTdhZTE0N2YzYzNiZSIsInNjb3BlIjpbIm9wZW5pZCJdLCJjbGllbnRfaWQiOiJkZWNvZGUtdG9rZW4tZGVtbyIsImNpZCI6ImRlY29kZS10b2tlbi1kZW1vIiwiYXpwIjoiZGVjb2RlLXRva2VuLWRlbW8iLCJncmFudF90eXBlIjoicGFzc3dvcmQiLCJ1c2VyX2lkIjoiMzk3OGZmNGQtZDczOC00YjhiLTk5MDgtN2FlMTQ3ZjNjM2JlIiwib3JpZ2luIjoidWFhIiwidXNlcl9uYW1lIjoibXl1c2VyIiwiZW1haWwiOiJteXVzZXJAZXhhbXBsZS5jb20iLCJhdXRoX3RpbWUiOjE1MzE2MzAxNDgsInJldl9zaWciOiI5M2E2NzkwNCIsImlhdCI6MTUzMTYzMDE0OCwiZXhwIjoxNTMxNjczMzQ4LCJpc3MiOiJodHRwczovLzE5Mi4xNjguNTAuNjo4NDQzL29hdXRoL3Rva2VuIiwiemlkIjoidWFhIiwiYXVkIjpbIm9wZW5pZCIsImRlY29kZS10b2tlbi1kZW1vIl19.qtbzxCOW5bebTgMLK-71_zxaT7l5PSmxhXcDtCeA64dZZ6-wXXmJivopm5PFEHnHiZwRpVe43jyEsbJGzBdl8GEsYQ9YIy51-4noby7ClziJv-6rSBYZnZuU5A234QRWclATGksOcz8Ft9PTIKGKLScyLhncwas7W0uiNJ87MFBGWY6Ovvl3Ac5-jHCqiRBXD6vUhzpfmy6_OUr53i9zJgtcQQWgDrOHxnFcRABZcDnhnWdcxh-Hbagtt9dQU46QgpqLJiUvAg-7ypZPGrxnr9UQEO2Q9GrolkbrSeUcfUOkgppxaA_0b6RYpgBR1qg-Ns6jGUxFgPs6Jj8pysfVmA", "token_type"=>"bearer", "refresh_token"=>"6701ddb9397840a1bd339e9f4314448f-r", "expires_in"=>43199, "scope"=>"openid", "jti"=>"e11efc06258d4304a78db279bb2c3458"}>
|
118
|
+
|
119
|
+
Auth header for resource server API calls: "bearer eyJhbGciOiJSUzI1NiIsImtpZCI6InVhYS1qd3Qta2V5LTEiLCJ0eXAiOiJKV1QifQ.eyJqdGkiOiJlMTFlZmMwNjI1OGQ0MzA0YTc4ZGIyNzliYjJjMzQ1OCIsInN1YiI6IjM5NzhmZjRkLWQ3MzgtNGI4Yi05OTA4LTdhZTE0N2YzYzNiZSIsInNjb3BlIjpbIm9wZW5pZCJdLCJjbGllbnRfaWQiOiJkZWNvZGUtdG9rZW4tZGVtbyIsImNpZCI6ImRlY29kZS10b2tlbi1kZW1vIiwiYXpwIjoiZGVjb2RlLXRva2VuLWRlbW8iLCJncmFudF90eXBlIjoicGFzc3dvcmQiLCJ1c2VyX2lkIjoiMzk3OGZmNGQtZDczOC00YjhiLTk5MDgtN2FlMTQ3ZjNjM2JlIiwib3JpZ2luIjoidWFhIiwidXNlcl9uYW1lIjoibXl1c2VyIiwiZW1haWwiOiJteXVzZXJAZXhhbXBsZS5jb20iLCJhdXRoX3RpbWUiOjE1MzE2MzAxNDgsInJldl9zaWciOiI5M2E2NzkwNCIsImlhdCI6MTUzMTYzMDE0OCwiZXhwIjoxNTMxNjczMzQ4LCJpc3MiOiJodHRwczovLzE5Mi4xNjguNTAuNjo4NDQzL29hdXRoL3Rva2VuIiwiemlkIjoidWFhIiwiYXVkIjpbIm9wZW5pZCIsImRlY29kZS10b2tlbi1kZW1vIl19.qtbzxCOW5bebTgMLK-71_zxaT7l5PSmxhXcDtCeA64dZZ6-wXXmJivopm5PFEHnHiZwRpVe43jyEsbJGzBdl8GEsYQ9YIy51-4noby7ClziJv-6rSBYZnZuU5A234QRWclATGksOcz8Ft9PTIKGKLScyLhncwas7W0uiNJ87MFBGWY6Ovvl3Ac5-jHCqiRBXD6vUhzpfmy6_OUr53i9zJgtcQQWgDrOHxnFcRABZcDnhnWdcxh-Hbagtt9dQU46QgpqLJiUvAg-7ypZPGrxnr9UQEO2Q9GrolkbrSeUcfUOkgppxaA_0b6RYpgBR1qg-Ns6jGUxFgPs6Jj8pysfVmA"
|
120
|
+
|
121
|
+
User info: {"user_id"=>"3978ff4d-d738-4b8b-9908-7ae147f3c3be", "user_name"=>"myuser", "name"=>"My User", "given_name"=>"My", "family_name"=>"User", "email"=>"myuser@example.com", "email_verified"=>true, "previous_logon_time"=>nil, "sub"=>"3978ff4d-d738-4b8b-9908-7ae147f3c3be"}
|
122
|
+
|
123
|
+
Decoded access token: {"jti"=>"e11efc06258d4304a78db279bb2c3458", "sub"=>"3978ff4d-d738-4b8b-9908-7ae147f3c3be", "scope"=>["openid"], "client_id"=>"decode-token-demo", "cid"=>"decode-token-demo", "azp"=>"decode-token-demo", "grant_type"=>"password", "user_id"=>"3978ff4d-d738-4b8b-9908-7ae147f3c3be", "origin"=>"uaa", "user_name"=>"myuser", "email"=>"myuser@example.com", "auth_time"=>1531630148, "rev_sig"=>"93a67904", "iat"=>1531630148, "exp"=>1531673348, "iss"=>"https://192.168.50.6:8443/oauth/token", "zid"=>"uaa", "aud"=>["openid", "decode-token-demo"]}
|
124
|
+
>>>>>>> 21ae635... new example script - password grant + decode using token keys
|
125
|
+
```
|
28
126
|
|
29
127
|
## Tests
|
30
128
|
|
31
129
|
Run the tests with rake:
|
32
130
|
|
33
|
-
|
131
|
+
```plain
|
132
|
+
bundle exec rake test
|
133
|
+
```
|
34
134
|
|
35
135
|
Run the tests and see a fancy coverage report:
|
36
136
|
|
37
|
-
|
137
|
+
```plain
|
138
|
+
bundle exec rake cov
|
139
|
+
```
|
38
140
|
|
data/cf-uaa-lib.gemspec
CHANGED
@@ -24,8 +24,6 @@ Gem::Specification.new do |s|
|
|
24
24
|
s.summary = %q{Client library for CloudFoundry UAA}
|
25
25
|
s.description = %q{Client library for interacting with the CloudFoundry User Account and Authorization (UAA) server. The UAA is an OAuth2 Authorization Server so it can be used by webapps and command line apps to obtain access tokens to act on behalf of users. The tokens can then be used to access protected resources in a Resource Server. This library is for use by UAA client applications or resource servers.}
|
26
26
|
|
27
|
-
s.rubyforge_project = "cf-uaa-lib"
|
28
|
-
|
29
27
|
s.license = "Apache-2.0"
|
30
28
|
s.files = `git ls-files`.split("\n")
|
31
29
|
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
@@ -33,15 +31,17 @@ Gem::Specification.new do |s|
|
|
33
31
|
s.require_paths = ['lib']
|
34
32
|
|
35
33
|
# dependencies
|
36
|
-
s.add_dependency 'multi_json', '
|
34
|
+
s.add_dependency 'multi_json', '>= 1.12.1', '< 1.16'
|
37
35
|
s.add_dependency 'httpclient', '~> 2.8', '>= 2.8.2.4'
|
36
|
+
s.add_dependency 'addressable', '~> 2.8', '>= 2.8.0'
|
38
37
|
|
39
|
-
s.add_development_dependency 'bundler', '~>
|
40
|
-
s.add_development_dependency 'rake', '
|
41
|
-
s.add_development_dependency 'rspec', '
|
42
|
-
s.add_development_dependency 'simplecov', '~> 0.
|
38
|
+
s.add_development_dependency 'bundler', '~> 2.2'
|
39
|
+
s.add_development_dependency 'rake', '>= 10.3.2', '~> 13.0'
|
40
|
+
s.add_development_dependency 'rspec', '>= 2.14.1', '~> 3.9'
|
41
|
+
s.add_development_dependency 'simplecov', '~> 0.21.2'
|
43
42
|
s.add_development_dependency 'simplecov-rcov', '~> 0.2.3'
|
44
|
-
s.add_development_dependency 'ci_reporter', '
|
45
|
-
s.add_development_dependency 'json_pure', '
|
43
|
+
s.add_development_dependency 'ci_reporter', '>= 1.9.2', '~> 2.0'
|
44
|
+
s.add_development_dependency 'json_pure', '>= 1.8.1', '~> 2.5'
|
45
|
+
s.add_development_dependency 'ci_reporter_rspec', '~> 1.0'
|
46
46
|
|
47
47
|
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
# uaa create-client decode-token-demo -s decode-token-demo -v \
|
4
|
+
# --authorized_grant_types password,refresh_token \
|
5
|
+
# --scope "openid" \
|
6
|
+
# --authorities uaa.none
|
7
|
+
|
8
|
+
require 'uaa'
|
9
|
+
|
10
|
+
url = ENV["UAA_URL"]
|
11
|
+
client, secret = "decode-token-demo", "decode-token-demo"
|
12
|
+
username, password = ENV["UAA_USERNAME"], ENV["UAA_PASSWORD"]
|
13
|
+
|
14
|
+
def show(title, object)
|
15
|
+
puts "#{title}: #{object.inspect}"
|
16
|
+
puts
|
17
|
+
end
|
18
|
+
|
19
|
+
uaa_options = {}
|
20
|
+
uaa_options[:ssl_ca_file] = ENV["UAA_CA_CERT_FILE"] if ENV["UAA_CA_CERT_FILE"]
|
21
|
+
show "uaa_options", uaa_options
|
22
|
+
|
23
|
+
uaa_info = CF::UAA::Info.new(url, uaa_options)
|
24
|
+
show "UAA server info", uaa_info.server
|
25
|
+
|
26
|
+
token_keys = uaa_info.validation_keys_hash
|
27
|
+
show "Signing keys for access tokens", token_keys
|
28
|
+
|
29
|
+
token_issuer = CF::UAA::TokenIssuer.new(url, client, secret, uaa_options)
|
30
|
+
show "Login prompts", token_issuer.prompts
|
31
|
+
|
32
|
+
token = token_issuer.owner_password_grant(username, password, "openid")
|
33
|
+
show "User '#{username}' password grant", token
|
34
|
+
|
35
|
+
auth_header = "bearer #{token.info["access_token"]}"
|
36
|
+
show "Auth header for resource server API calls", auth_header
|
37
|
+
|
38
|
+
userinfo = uaa_info.whoami(auth_header)
|
39
|
+
show "User info", userinfo
|
40
|
+
|
41
|
+
last_exception = nil
|
42
|
+
token_keys.each_pair do |keyname, token_key|
|
43
|
+
begin
|
44
|
+
token_coder = CF::UAA::TokenCoder.new(uaa_options.merge(pkey: token_key["value"], verify: true))
|
45
|
+
token_info = token_coder.decode(auth_header)
|
46
|
+
show "Decoded access token", token_info
|
47
|
+
last_exception = nil
|
48
|
+
rescue CF::UAA::Decode => e
|
49
|
+
last_exception = e
|
50
|
+
end
|
51
|
+
end
|
52
|
+
raise last_exception if last_exception
|
53
|
+
|
data/lib/uaa/http.rb
CHANGED
@@ -183,6 +183,8 @@ module Http
|
|
183
183
|
raise SSLException, "Invalid SSL Cert for #{url}. Use '--skip-ssl-validation' to continue with an insecure target"
|
184
184
|
rescue URI::Error, SocketError, SystemCallError => e
|
185
185
|
raise BadTarget, "error: #{e.message}"
|
186
|
+
rescue HTTPClient::ConnectTimeoutError => e
|
187
|
+
raise HTTPException.new "http timeout"
|
186
188
|
end
|
187
189
|
|
188
190
|
def http_request(uri)
|
data/lib/uaa/info.rb
CHANGED
@@ -54,14 +54,6 @@ class Info
|
|
54
54
|
json_get(target, "/userinfo?schema=openid", key_style, "authorization" => auth_header)
|
55
55
|
end
|
56
56
|
|
57
|
-
# Gets various monitoring and status variables from the server.
|
58
|
-
# Authenticates using +name+ and +pwd+ for basic authentication.
|
59
|
-
# @param (see Misc.server)
|
60
|
-
# @return [Hash]
|
61
|
-
def varz(name, pwd)
|
62
|
-
json_get(target, "/varz", key_style, "authorization" => Http.basic_auth(name, pwd))
|
63
|
-
end
|
64
|
-
|
65
57
|
# Gets basic information about the target server, including version number,
|
66
58
|
# commit ID, and links to API endpoints.
|
67
59
|
# @return [Hash]
|
@@ -129,7 +121,7 @@ class Info
|
|
129
121
|
# @return [Hash] contents of the token
|
130
122
|
def decode_token(client_id, client_secret, token, token_type = "bearer", audience_ids = nil)
|
131
123
|
reply = json_parse_reply(key_style, *request(target, :post, '/check_token',
|
132
|
-
Util.encode_form(:
|
124
|
+
Util.encode_form(token: token),
|
133
125
|
"authorization" => Http.basic_auth(client_id, client_secret),
|
134
126
|
"content-type" => Http::FORM_UTF8,"accept" => Http::JSON_UTF8))
|
135
127
|
|
@@ -146,7 +138,7 @@ class Info
|
|
146
138
|
# @return [Hash]
|
147
139
|
def password_strength(password)
|
148
140
|
json_parse_reply(key_style, *request(target, :post, '/password/score',
|
149
|
-
Util.encode_form(:
|
141
|
+
Util.encode_form(password: password), "content-type" => Http::FORM_UTF8,
|
150
142
|
"accept" => Http::JSON_UTF8))
|
151
143
|
end
|
152
144
|
|
data/lib/uaa/scim.rb
CHANGED
@@ -12,7 +12,7 @@
|
|
12
12
|
#++
|
13
13
|
|
14
14
|
require 'uaa/http'
|
15
|
-
require 'uri'
|
15
|
+
require 'addressable/uri'
|
16
16
|
|
17
17
|
module CF::UAA
|
18
18
|
|
@@ -177,7 +177,7 @@ class Scim
|
|
177
177
|
# @param [String] id the id attribute of the SCIM object
|
178
178
|
# @return [nil]
|
179
179
|
def delete(type, id)
|
180
|
-
http_delete @target, "#{type_info(type, :path)}/#{URI.encode(id)}", @auth_header, @zone
|
180
|
+
http_delete @target, "#{type_info(type, :path)}/#{Addressable::URI.encode(id)}", @auth_header, @zone
|
181
181
|
end
|
182
182
|
|
183
183
|
# Replaces the contents of a SCIM object.
|
@@ -192,7 +192,7 @@ class Scim
|
|
192
192
|
hdrs.merge!('if-match' => etag)
|
193
193
|
end
|
194
194
|
reply = json_parse_reply(@key_style,
|
195
|
-
*json_put(@target, "#{path}/#{URI.encode(id)}", info, hdrs))
|
195
|
+
*json_put(@target, "#{path}/#{Addressable::URI.encode(id)}", info, hdrs))
|
196
196
|
|
197
197
|
# hide client endpoints that are not quite scim compatible
|
198
198
|
type == :client && !reply ? get(type, info['client_id']): reply
|
@@ -210,7 +210,7 @@ class Scim
|
|
210
210
|
hdrs.merge!('if-match' => etag)
|
211
211
|
end
|
212
212
|
reply = json_parse_reply(@key_style,
|
213
|
-
*json_patch(@target, "#{path}/#{URI.encode(id)}", info, hdrs))
|
213
|
+
*json_patch(@target, "#{path}/#{Addressable::URI.encode(id)}", info, hdrs))
|
214
214
|
|
215
215
|
# hide client endpoints that are not quite scim compatible
|
216
216
|
type == :client && !reply ? get(type, info['client_id']): reply
|
@@ -258,7 +258,7 @@ class Scim
|
|
258
258
|
# @param (see #delete)
|
259
259
|
# @return (see #add)
|
260
260
|
def get(type, id)
|
261
|
-
info = json_get(@target, "#{type_info(type, :path)}/#{URI.encode(id)}",
|
261
|
+
info = json_get(@target, "#{type_info(type, :path)}/#{Addressable::URI.encode(id)}",
|
262
262
|
@key_style, headers)
|
263
263
|
|
264
264
|
fake_client_id(info) if type == :client # hide client reply, not quite scim
|
@@ -270,7 +270,7 @@ class Scim
|
|
270
270
|
# @return (client meta)
|
271
271
|
def get_client_meta(client_id)
|
272
272
|
path = type_info(:client, :path)
|
273
|
-
json_get(@target, "#{path}/#{URI.encode(client_id)}/meta", @key_style, headers)
|
273
|
+
json_get(@target, "#{path}/#{Addressable::URI.encode(client_id)}/meta", @key_style, headers)
|
274
274
|
end
|
275
275
|
|
276
276
|
# Collects all pages of entries from a query
|
@@ -350,7 +350,7 @@ class Scim
|
|
350
350
|
req = {"password" => new_password}
|
351
351
|
req["oldPassword"] = old_password if old_password
|
352
352
|
json_parse_reply(@key_style, *json_put(@target,
|
353
|
-
"#{type_info(:user, :path)}/#{URI.encode(user_id)}/password", req, headers))
|
353
|
+
"#{type_info(:user, :path)}/#{Addressable::URI.encode(user_id)}/password", req, headers))
|
354
354
|
end
|
355
355
|
|
356
356
|
# Change client secret.
|
@@ -366,18 +366,18 @@ class Scim
|
|
366
366
|
req = {"secret" => new_secret }
|
367
367
|
req["oldSecret"] = old_secret if old_secret
|
368
368
|
json_parse_reply(@key_style, *json_put(@target,
|
369
|
-
"#{type_info(:client, :path)}/#{URI.encode(client_id)}/secret", req, headers))
|
369
|
+
"#{type_info(:client, :path)}/#{Addressable::URI.encode(client_id)}/secret", req, headers))
|
370
370
|
end
|
371
371
|
|
372
372
|
def unlock_user(user_id)
|
373
373
|
req = {"locked" => false}
|
374
374
|
json_parse_reply(@key_style, *json_patch(@target,
|
375
|
-
"#{type_info(:user, :path)}/#{URI.encode(user_id)}/status", req, headers))
|
375
|
+
"#{type_info(:user, :path)}/#{Addressable::URI.encode(user_id)}/status", req, headers))
|
376
376
|
end
|
377
377
|
|
378
378
|
def map_group(group, is_id, external_group, origin = "ldap")
|
379
379
|
key_name = is_id ? :groupId : :displayName
|
380
|
-
request = {key_name => group, :
|
380
|
+
request = {key_name => group, externalGroup: external_group, schemas: ["urn:scim:schemas:core:1.0"], origin: origin }
|
381
381
|
result = json_parse_reply(@key_style, *json_post(@target,
|
382
382
|
"#{type_info(:group_mapping, :path)}", request,
|
383
383
|
headers))
|
@@ -385,7 +385,7 @@ class Scim
|
|
385
385
|
end
|
386
386
|
|
387
387
|
def unmap_group(group_id, external_group, origin = "ldap")
|
388
|
-
http_delete(@target, "#{type_info(:group_mapping, :path)}/groupId/#{group_id}/externalGroup/#{URI.encode(external_group)}/origin/#{origin}",
|
388
|
+
http_delete(@target, "#{type_info(:group_mapping, :path)}/groupId/#{group_id}/externalGroup/#{Addressable::URI.encode(external_group)}/origin/#{origin}",
|
389
389
|
@auth_header, @zone)
|
390
390
|
end
|
391
391
|
|
data/lib/uaa/token_coder.rb
CHANGED
@@ -64,8 +64,8 @@ class TokenCoder
|
|
64
64
|
def self.encode(token_body, options = {}, obsolete1 = nil, obsolete2 = nil)
|
65
65
|
unless options.is_a?(Hash) && obsolete1.nil? && obsolete2.nil?
|
66
66
|
# deprecated: def self.encode(token_body, skey, pkey = nil, algo = 'HS256')
|
67
|
-
warn "#{self.class}##{__method__} is deprecated with these parameters. Please use options hash."
|
68
|
-
options = {:
|
67
|
+
warn "WARNING: #{self.class}##{__method__} is deprecated with these parameters. Please use options hash."
|
68
|
+
options = {skey: options}
|
69
69
|
options[:pkey], options[:algorithm] = obsolete1, obsolete2
|
70
70
|
end
|
71
71
|
options = normalize_options(options)
|
@@ -95,8 +95,8 @@ class TokenCoder
|
|
95
95
|
def self.decode(token, options = {}, obsolete1 = nil, obsolete2 = nil)
|
96
96
|
unless options.is_a?(Hash) && obsolete1.nil? && obsolete2.nil?
|
97
97
|
# deprecated: def self.decode(token, skey = nil, pkey = nil, verify = true)
|
98
|
-
warn "#{self.class}##{__method__} is deprecated with these parameters. Please use options hash."
|
99
|
-
options = {:
|
98
|
+
warn "WARNING: #{self.class}##{__method__} is deprecated with these parameters. Please use options hash."
|
99
|
+
options = {skey: options}
|
100
100
|
options[:pkey], options[:verify] = obsolete1, obsolete2
|
101
101
|
end
|
102
102
|
options = normalize_options(options)
|
@@ -106,10 +106,16 @@ class TokenCoder
|
|
106
106
|
signing_input = [header_segment, payload_segment].join('.')
|
107
107
|
header = Util.json_decode64(header_segment)
|
108
108
|
payload = Util.json_decode64(payload_segment, (:sym if options[:symbolize_keys]))
|
109
|
-
|
109
|
+
unless options[:verify]
|
110
|
+
warn "WARNING: Decoding token without verifying it was signed by its authoring UAA"
|
111
|
+
return payload
|
112
|
+
end
|
110
113
|
raise SignatureNotAccepted, "Signature algorithm not accepted" unless
|
111
114
|
options[:accept_algorithms].include?(algo = header["alg"])
|
112
|
-
|
115
|
+
if algo == 'none'
|
116
|
+
warn "WARNING: Decoding token that explicitly states it has not been signed by an authoring UAA"
|
117
|
+
return payload
|
118
|
+
end
|
113
119
|
signature = Util.decode64(crypto_segment)
|
114
120
|
if ["HS256", "HS384", "HS512"].include?(algo)
|
115
121
|
raise InvalidSignature, "Signature verification failed" unless
|
@@ -123,6 +129,17 @@ class TokenCoder
|
|
123
129
|
payload
|
124
130
|
end
|
125
131
|
|
132
|
+
# Decodes a JWT token to extract its expiry time
|
133
|
+
# @param [String] token A JWT token as returned by {TokenCoder.encode}
|
134
|
+
# @return [Integer] exp expiry timestamp
|
135
|
+
def self.decode_token_expiry(token)
|
136
|
+
segments = token.split('.')
|
137
|
+
raise InvalidTokenFormat, "Not enough or too many segments" unless [2,3].include? segments.length
|
138
|
+
header_segment, payload_segment, crypto_segment = segments
|
139
|
+
payload = Util.json_decode64(payload_segment, :sym)
|
140
|
+
payload[:exp]
|
141
|
+
end
|
142
|
+
|
126
143
|
# Takes constant time to compare 2 strings (HMAC digests in this case)
|
127
144
|
# to avoid timing attacks while comparing the HMAC digests
|
128
145
|
# @param [String] a: the first digest to compare
|
@@ -170,7 +187,7 @@ class TokenCoder
|
|
170
187
|
unless options.is_a?(Hash) && obsolete1.nil? && obsolete2.nil?
|
171
188
|
# deprecated: def initialize(audience_ids, skey, pkey = nil)
|
172
189
|
warn "#{self.class}##{__method__} is deprecated with these parameters. Please use options hash."
|
173
|
-
options = {:
|
190
|
+
options = {audience_ids: options }
|
174
191
|
options[:skey], options[:pkey] = obsolete1, obsolete2
|
175
192
|
end
|
176
193
|
@options = self.class.normalize_options(options)
|
@@ -184,7 +201,7 @@ class TokenCoder
|
|
184
201
|
def encode(token_body = {}, algorithm = nil)
|
185
202
|
token_body[:aud] = @options[:audience_ids] if @options[:audience_ids] && !token_body[:aud] && !token_body['aud']
|
186
203
|
token_body[:exp] = Time.now.to_i + 7 * 24 * 60 * 60 unless token_body[:exp] || token_body['exp']
|
187
|
-
self.class.encode(token_body, algorithm ? @options.merge(:
|
204
|
+
self.class.encode(token_body, algorithm ? @options.merge(algorithm: algorithm) : @options)
|
188
205
|
end
|
189
206
|
|
190
207
|
# Returns hash of values decoded from the token contents. If the
|
data/lib/uaa/token_issuer.rb
CHANGED
@@ -13,6 +13,7 @@
|
|
13
13
|
|
14
14
|
require 'securerandom'
|
15
15
|
require 'uaa/http'
|
16
|
+
require 'cgi'
|
16
17
|
|
17
18
|
module CF::UAA
|
18
19
|
|
@@ -72,8 +73,13 @@ class TokenIssuer
|
|
72
73
|
if scope = Util.arglist(params.delete(:scope))
|
73
74
|
params[:scope] = Util.strlist(scope)
|
74
75
|
end
|
75
|
-
headers = {'content-type' => FORM_UTF8, 'accept' => JSON_UTF8
|
76
|
-
|
76
|
+
headers = {'content-type' => FORM_UTF8, 'accept' => JSON_UTF8}
|
77
|
+
if @basic_auth
|
78
|
+
headers['authorization'] = Http.basic_auth(@client_id, @client_secret)
|
79
|
+
else
|
80
|
+
headers['X-CF-ENCODED-CREDENTIALS'] = 'true'
|
81
|
+
headers['authorization'] = Http.basic_auth(CGI.escape(@client_id || ''), CGI.escape(@client_secret || ''))
|
82
|
+
end
|
77
83
|
reply = json_parse_reply(@key_style, *request(@token_target, :post,
|
78
84
|
'/oauth/token', Util.encode_form(params), headers))
|
79
85
|
raise BadResponse unless reply[jkey :token_type] && reply[jkey :access_token]
|
@@ -81,8 +87,8 @@ class TokenIssuer
|
|
81
87
|
end
|
82
88
|
|
83
89
|
def authorize_path_args(response_type, redirect_uri, scope, state = random_state, args = {})
|
84
|
-
params = args.merge(:
|
85
|
-
:
|
90
|
+
params = args.merge(client_id: @client_id, response_type: response_type,
|
91
|
+
redirect_uri: redirect_uri, state: state)
|
86
92
|
params[:scope] = scope = Util.strlist(scope) if scope = Util.arglist(scope)
|
87
93
|
params[:nonce] = state
|
88
94
|
"/oauth/authorize?#{Util.encode_form(params)}"
|
@@ -109,6 +115,7 @@ class TokenIssuer
|
|
109
115
|
@target, @client_id, @client_secret = target, client_id, client_secret
|
110
116
|
@token_target = options[:token_target] || target
|
111
117
|
@key_style = options[:symbolize_keys] ? :sym : nil
|
118
|
+
@basic_auth = options[:basic_auth] == true ? true : false
|
112
119
|
initialize_http_options(options)
|
113
120
|
end
|
114
121
|
|
@@ -137,7 +144,7 @@ class TokenIssuer
|
|
137
144
|
|
138
145
|
# the accept header is only here so the uaa will issue error replies in json to aid debugging
|
139
146
|
headers = {'content-type' => FORM_UTF8, 'accept' => JSON_UTF8 }
|
140
|
-
body = Util.encode_form(credentials.merge(:
|
147
|
+
body = Util.encode_form(credentials.merge(source: 'credentials'))
|
141
148
|
status, body, headers = request(@target, :post, uri, body, headers)
|
142
149
|
raise BadResponse, "status #{status}" unless status == 302
|
143
150
|
req_uri, reply_uri = URI.parse(redir_uri), URI.parse(headers['location'])
|
@@ -194,7 +201,7 @@ class TokenIssuer
|
|
194
201
|
reply = json_parse_reply(nil, *request(@target, :post, "/autologin", body, headers))
|
195
202
|
raise BadResponse, "no autologin code in reply" unless reply['code']
|
196
203
|
@target + authorize_path_args('code', redirect_uri, scope,
|
197
|
-
random_state, :
|
204
|
+
random_state, code: reply['code'])
|
198
205
|
end
|
199
206
|
|
200
207
|
# Constructs a uri that the client is to return to the browser to direct
|
@@ -228,8 +235,8 @@ class TokenIssuer
|
|
228
235
|
rescue URI::InvalidURIError, ArgumentError, BadResponse
|
229
236
|
raise BadResponse, "received invalid response from target #{@target}"
|
230
237
|
end
|
231
|
-
request_token(:
|
232
|
-
:
|
238
|
+
request_token(grant_type: 'authorization_code', code: authcode,
|
239
|
+
redirect_uri: ac_params['redirect_uri'])
|
233
240
|
end
|
234
241
|
|
235
242
|
# Uses the instance client credentials in addition to the +username+
|
@@ -237,15 +244,15 @@ class TokenIssuer
|
|
237
244
|
# See {http://tools.ietf.org/html/rfc6749#section-4.3}.
|
238
245
|
# @return [TokenInfo]
|
239
246
|
def owner_password_grant(username, password, scope = nil)
|
240
|
-
request_token(:
|
241
|
-
:
|
247
|
+
request_token(grant_type: 'password', username: username,
|
248
|
+
password: password, scope: scope)
|
242
249
|
end
|
243
250
|
|
244
251
|
# Uses a one-time passcode obtained from the UAA to get a
|
245
252
|
# token.
|
246
253
|
# @return [TokenInfo]
|
247
254
|
def passcode_grant(passcode, scope = nil)
|
248
|
-
request_token(:
|
255
|
+
request_token(grant_type: 'password', passcode: passcode, scope: scope)
|
249
256
|
end
|
250
257
|
|
251
258
|
# Gets an access token with the user credentials used for authentication
|
@@ -264,14 +271,14 @@ class TokenIssuer
|
|
264
271
|
# credentials grant. See http://tools.ietf.org/html/rfc6749#section-4.4
|
265
272
|
# @return [TokenInfo]
|
266
273
|
def client_credentials_grant(scope = nil)
|
267
|
-
request_token(:
|
274
|
+
request_token(grant_type: 'client_credentials', scope: scope)
|
268
275
|
end
|
269
276
|
|
270
277
|
# Uses the instance client credentials and the given +refresh_token+ to get
|
271
278
|
# a new access token. See http://tools.ietf.org/html/rfc6749#section-6
|
272
279
|
# @return [TokenInfo] which may include a new refresh token as well as an access token.
|
273
280
|
def refresh_token_grant(refresh_token, scope = nil)
|
274
|
-
request_token(:
|
281
|
+
request_token(grant_type: 'refresh_token', refresh_token: refresh_token, scope: scope)
|
275
282
|
end
|
276
283
|
|
277
284
|
end
|