Import upstream version 2.6.0, md5 41676aee53b4e0fd056a545a0dc04b7c
Debian Janitor
3 years ago
0 | version: 2.1 | |
1 | matrix_rubyversions: &matrix_rubyversions | |
2 | matrix: | |
3 | parameters: | |
4 | rubyversion: ["2.5", "2.6", "2.7", "3.0"] | |
5 | # Default version of ruby to use for lint and publishing | |
6 | default_rubyversion: &default_rubyversion "2.7" | |
7 | ||
8 | executors: | |
9 | ruby: | |
10 | parameters: | |
11 | rubyversion: | |
12 | type: string | |
13 | default: *default_rubyversion | |
14 | docker: | |
15 | - image: circleci/ruby:<< parameters.rubyversion >> | |
16 | ||
17 | jobs: | |
18 | run-tests: | |
19 | parameters: | |
20 | rubyversion: | |
21 | type: string | |
22 | default: *default_rubyversion | |
23 | executor: | |
24 | name: ruby | |
25 | rubyversion: "<< parameters.rubyversion >>" | |
26 | steps: | |
27 | - checkout | |
28 | - restore_cache: | |
29 | keys: | |
30 | - gems-v2-{{ checksum "Gemfile" }} | |
31 | - gems-v2- | |
32 | - run: bundle check || bundle install | |
33 | - save_cache: | |
34 | key: gems-v2--{{ checksum "Gemfile" }} | |
35 | paths: | |
36 | - vendor/bundle | |
37 | - run: bundle exec rake spec | |
38 | ||
39 | workflows: | |
40 | tests: | |
41 | jobs: | |
42 | - run-tests: | |
43 | <<: *matrix_rubyversions |
0 | * @auth0/dx-sdks-engineer |
0 | blank_issues_enabled: false | |
1 | contact_links: | |
2 | - name: Auth0 Community | |
3 | url: https://community.auth0.com/c/sdks/5 | |
4 | about: Discuss this SDK in the Auth0 Community forums | |
5 | - name: Library Documentation | |
6 | url: https://github.com/auth0/omniauth-auth0#documentation | |
7 | about: Read the library docs on Auth0.com |
0 | --- | |
1 | name: Feature request | |
2 | about: Suggest an idea or a feature for this project | |
3 | title: '' | |
4 | labels: feature request | |
5 | assignees: '' | |
6 | --- | |
7 | ||
8 | <!-- | |
9 | **Please do not report security vulnerabilities here**. The Responsible Disclosure Program (https://auth0.com/whitehat) details the procedure for disclosing security issues. | |
10 | ||
11 | Thank you in advance for helping us to improve this library! Your attention to detail here is greatly appreciated and will help us respond as quickly as possible. For general support or usage questions, use the Auth0 Community (https://community.auth0.com/) or Auth0 Support (https://support.auth0.com/). Finally, to avoid duplicates, please search existing Issues before submitting one here. | |
12 | ||
13 | By submitting an Issue to this repository, you agree to the terms within the Auth0 Code of Conduct (https://github.com/auth0/open-source-template/blob/master/CODE-OF-CONDUCT.md). | |
14 | --> | |
15 | ||
16 | ### Describe the problem you'd like to have solved | |
17 | ||
18 | <!-- | |
19 | > A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] | |
20 | --> | |
21 | ||
22 | ### Describe the ideal solution | |
23 | ||
24 | <!-- | |
25 | > A clear and concise description of what you want to happen. | |
26 | --> | |
27 | ||
28 | ## Alternatives and current work-arounds | |
29 | ||
30 | <!-- | |
31 | > A clear and concise description of any alternatives you've considered or any work-arounds that are currently in place. | |
32 | --> | |
33 | ||
34 | ### Additional information, if any | |
35 | ||
36 | <!-- | |
37 | > Add any other context or screenshots about the feature request here. | |
38 | -->⏎ |
0 | --- | |
1 | name: Report a bug | |
2 | about: Have you found a bug or issue? Create a bug report for this SDK | |
3 | title: '' | |
4 | labels: bug report | |
5 | assignees: '' | |
6 | --- | |
7 | ||
8 | <!-- | |
9 | **Please do not report security vulnerabilities here**. The Responsible Disclosure Program (https://auth0.com/whitehat) details the procedure for disclosing security issues. | |
10 | ||
11 | Thank you in advance for helping us to improve this library! Please read through the template below and answer all relevant questions. Your additional work here is greatly appreciated and will help us respond as quickly as possible. For general support or usage questions, use the Auth0 Community (https://community.auth0.com/) or Auth0 Support (https://support.auth0.com/). Finally, to avoid duplicates, please search existing Issues before submitting one here. | |
12 | ||
13 | By submitting an Issue to this repository, you agree to the terms within the Auth0 Code of Conduct (https://github.com/auth0/open-source-template/blob/master/CODE-OF-CONDUCT.md). | |
14 | --> | |
15 | ||
16 | ### Describe the problem | |
17 | ||
18 | <!-- | |
19 | > Provide a clear and concise description of the issue | |
20 | --> | |
21 | ||
22 | ### What was the expected behavior? | |
23 | ||
24 | <!-- | |
25 | > Tell us about the behavior you expected to see | |
26 | --> | |
27 | ||
28 | ### Reproduction | |
29 | <!-- | |
30 | > Detail the steps taken to reproduce this error, and whether this issue can be reproduced consistently or if it is intermittent. | |
31 | > **Note**: If clear, reproducable steps or the smallest sample app demonstrating misbehavior cannot be provided, we may not be able to follow up on this bug report. | |
32 | ||
33 | > Where possible, please include: | |
34 | > | |
35 | > - The smallest possible sample app that reproduces the undesirable behavior | |
36 | > - Log files (redact/remove sensitive information) | |
37 | > - Application settings (redact/remove sensitive information) | |
38 | > - Screenshots | |
39 | --> | |
40 | ||
41 | - Step 1.. | |
42 | - Step 2.. | |
43 | - ... | |
44 | ||
45 | ### Environment | |
46 | ||
47 | <!-- | |
48 | > Please provide the following: | |
49 | --> | |
50 | ||
51 | - **Version of this library used:** | |
52 | - **Which framework are you using, if applicable:** | |
53 | - **Other modules/plugins/libraries that might be involved:** | |
54 | - **Any other relevant information you think would be useful:**⏎ |
0 | ### Changes | |
1 | ||
2 | Please describe both what is changing and why this is important. Include: | |
3 | ||
4 | - Endpoints added, deleted, deprecated, or changed | |
5 | - Classes and methods added, deleted, deprecated, or changed | |
6 | - Screenshots of new or changed UI, if applicable | |
7 | - A summary of usage if this is a new feature or change to a public API (this should also be added to relevant documentation once released) | |
8 | ||
9 | ### References | |
10 | ||
11 | Please include relevant links supporting this change such as a: | |
12 | ||
13 | - support ticket | |
14 | - community post | |
15 | - StackOverflow post | |
16 | - support forum thread | |
17 | - related GitHub issue in this or another repo | |
18 | ||
19 | ### Testing | |
20 | ||
21 | Please describe how this can be tested by reviewers. Be specific about anything not tested and reasons why. If this library has unit and/or integration testing, tests should be added for new functionality and existing tests should complete without errors. | |
22 | ||
23 | * [ ] This change adds unit test coverage | |
24 | * [ ] This change has been tested on the latest version of the platform/language or why not | |
25 | ||
26 | ### Checklist | |
27 | ||
28 | * [ ] I have read the [Auth0 contribution guidelines](https://github.com/auth0/open-source-template/blob/master/GENERAL-CONTRIBUTING.md) | |
29 | * [ ] I have read the [Auth0 Code of Conduct](https://github.com/auth0/open-source-template/blob/master/CODE-OF-CONDUCT.md) | |
30 | * [ ] All existing and new tests complete without errors | |
31 | * [ ] All code quality tools/guidelines in the [CONTRIBUTING documentation](https://github.com/auth0/omniauth-auth0/blob/master/CONTRIBUTING.md) have been run/followed |
0 | # Configuration for probot-stale - https://github.com/probot/stale | |
1 | ||
2 | # Number of days of inactivity before an Issue or Pull Request becomes stale | |
3 | daysUntilStale: 90 | |
4 | ||
5 | # Number of days of inactivity before an Issue or Pull Request with the stale label is closed. | |
6 | daysUntilClose: 7 | |
7 | ||
8 | # Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable | |
9 | exemptLabels: [] | |
10 | ||
11 | # Set to true to ignore issues with an assignee (defaults to false) | |
12 | exemptAssignees: true | |
13 | ||
14 | # Label to use when marking as stale | |
15 | staleLabel: closed:stale | |
16 | ||
17 | # Comment to post when marking as stale. Set to `false` to disable | |
18 | markComment: > | |
19 | This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. If you have not received a response for our team (apologies for the delay) and this is still a blocker, please reply with additional information or just a ping. Thank you for your contribution! 🙇♂️⏎ |
0 | 0 | .ruby-version |
1 | 1 | coverage |
2 | Gemfile.lock | |
3 | 2 | *.gem |
4 | 3 | |
5 | 4 | .#* |
6 | 5 | .env |
7 | 6 | log/ |
8 | tmp/⏎ | |
7 | tmp/ | |
8 | ||
9 | ## Environment normalization: | |
10 | /.bundle | |
11 | /vendor/bundle | |
12 | ||
13 | Gemfile.lock |
0 | # Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities. | |
1 | version: v1.13.5 | |
2 | # ignores vulnerabilities until expiry date; change duration by modifying expiry date | |
3 | ignore: | |
4 | SNYK-RUBY-OMNIAUTH-174820: | |
5 | - '*': | |
6 | reason: Not affected. | |
7 | expires: 2020-01-01T00:00:00.000Z | |
8 | patch: {} |
0 | 0 | # Change Log |
1 | ||
2 | ## [v2.6.0](https://github.com/auth0/omniauth-auth0/tree/v2.6.0) (2021-04-01) | |
3 | ||
4 | [Full Changelog](https://github.com/auth0/omniauth-auth0/compare/v2.5.0...v2.6.0) | |
5 | ||
6 | **Added** | |
7 | - Org Support [SDK-2395] [\#124](https://github.com/auth0/omniauth-auth0/pull/124) ([davidpatrick](https://github.com/davidpatrick)) | |
8 | - Add login_hint to permitted params [\#123](https://github.com/auth0/omniauth-auth0/pull/123) ([Roriz](https://github.com/Roriz)) | |
9 | ||
10 | ## [v2.5.0](https://github.com/auth0/omniauth-auth0/tree/v2.5.0) (2021-01-21) | |
11 | ||
12 | [Full Changelog](https://github.com/auth0/omniauth-auth0/compare/v2.4.2...v2.5.0) | |
13 | ||
14 | **Added** | |
15 | - Parsing claims from the id_token [\#120](https://github.com/auth0/omniauth-auth0/pull/120) ([davidpatrick](https://github.com/davidpatrick)) | |
16 | ||
17 | **Changed** | |
18 | - Setup build matrix in CI [\#116](https://github.com/auth0/omniauth-auth0/pull/116) ([dmathieu](https://github.com/dmathieu)) | |
19 | ||
20 | **Fixed** | |
21 | - Fixes params passed to authorize [\#119](https://github.com/auth0/omniauth-auth0/pull/119) ([davidpatrick](https://github.com/davidpatrick)) | |
22 | ||
23 | ||
24 | ## [v2.4.2](https://github.com/auth0/omniauth-auth0/tree/v2.4.2) (2021-01-19) | |
25 | ||
26 | [Full Changelog](https://github.com/auth0/omniauth-auth0/compare/v2.4.1...v2.4.2) | |
27 | ||
28 | **Fixed** | |
29 | - Lock Omniauth to 1.9 in gemspec | |
30 | ||
31 | ## [v2.4.1](https://github.com/auth0/omniauth-auth0/tree/v2.4.1) (2020-10-08) | |
32 | ||
33 | [Full Changelog](https://github.com/auth0/omniauth-auth0/compare/v2.4.0...v2.4.1) | |
34 | ||
35 | **Fixed** | |
36 | - Verify the JWT Signature [\#109](https://github.com/auth0/omniauth-auth0/pull/109) ([jimmyjames](https://github.com/jimmyjames)) | |
37 | ||
38 | ||
39 | ## [v2.4.0](https://github.com/auth0/omniauth-auth0/tree/v2.4.0) (2020-09-22) | |
40 | ||
41 | [Full Changelog](https://github.com/auth0/omniauth-auth0/compare/v2.3.1...v2.4.0) | |
42 | ||
43 | **Security** | |
44 | - Bump rack from 2.2.2 to 2.2.3 [\#107](https://github.com/auth0/omniauth-auth0/pull/107) ([dependabot](https://github.com/dependabot)) | |
45 | - Update dependencies [\#100](https://github.com/auth0/omniauth-auth0/pull/100) ([Albalmaceda](https://github.com/Albalmaceda)) | |
46 | ||
47 | **Added** | |
48 | - Add support for screen_hint=signup param [\#103](https://github.com/auth0/omniauth-auth0/pull/103) ([bbean86](https://github.com/bbean86)) | |
49 | - Add support for `connection_scope` in params [\#99](https://github.com/auth0/omniauth-auth0/pull/99) ([felixclack](https://github.com/felixclack)) | |
50 | ||
51 | ||
52 | ## [v2.3.1](https://github.com/auth0/omniauth-auth0/tree/v2.3.1) (2020-03-27) | |
53 | ||
54 | [Full Changelog](https://github.com/auth0/omniauth-auth0/compare/v2.3.0...v2.3.1) | |
55 | ||
56 | **Fixed bugs:** | |
57 | ||
58 | - Fixes dependency issue [\#97](https://github.com/auth0/omniauth-auth0/pull/97) ([davidpatrick](https://github.com/davidpatrick)) | |
59 | - Fix "NameError: uninitialized constant OmniAuth::Auth0::TokenValidationError" [\#96](https://github.com/auth0/omniauth-auth0/pull/96) ([stefanwork](https://github.com/stefanwork)) | |
60 | ||
61 | ## [v2.3.0](https://github.com/auth0/omniauth-auth0/tree/v2.3.0) (2020-03-06) | |
62 | [Full Changelog](https://github.com/auth0/omniauth-auth0/compare/v2.2.0...v2.3.0) | |
63 | ||
64 | **Added** | |
65 | - Improved OIDC Compliance [\#92](https://github.com/auth0/omniauth-auth0/pull/92) ([davidpatrick](https://github.com/davidpatrick)) | |
66 | ||
67 | ## [v2.2.0](https://github.com/auth0/omniauth-auth0/tree/v2.2.0) (2018-04-18) | |
68 | [Full Changelog](https://github.com/auth0/omniauth-auth0/compare/v2.1.0...v2.2.0) | |
69 | ||
70 | **Closed issues** | |
71 | - It supports custom domain? [\#71](https://github.com/auth0/omniauth-auth0/issues/71) | |
72 | - Valid Login, No Details: email=nil image=nil name="github|38257089" nickname=nil [\#70](https://github.com/auth0/omniauth-auth0/issues/70) | |
73 | ||
74 | **Added** | |
75 | - Custom issuer [\#77](https://github.com/auth0/omniauth-auth0/pull/77) ([ryan-rosenfeld](https://github.com/ryan-rosenfeld)) | |
76 | - Add telemetry to token endpoint [\#74](https://github.com/auth0/omniauth-auth0/pull/74) ([joshcanhelp](https://github.com/joshcanhelp)) | |
77 | ||
78 | **Changed** | |
79 | - Remove telemetry from authorize URL [\#75](https://github.com/auth0/omniauth-auth0/pull/75) ([joshcanhelp](https://github.com/joshcanhelp)) | |
80 | ||
81 | ## [v2.1.0](https://github.com/auth0/omniauth-auth0/tree/v2.1.0) (2018-10-30) | |
82 | [Full Changelog](https://github.com/auth0/omniauth-auth0/compare/v2.0.0...v2.1.0) | |
83 | ||
84 | **Closed issues** | |
85 | - URL should be spelled uppercase outside of code [\#64](https://github.com/auth0/omniauth-auth0/issues/64) | |
86 | - Add prompt=none authorization param handler [\#58](https://github.com/auth0/omniauth-auth0/issues/58) | |
87 | - Could not find a valid mapping for path "/auth/oauth2/callback" [\#56](https://github.com/auth0/omniauth-auth0/issues/56) | |
88 | - I had to downgrade my gems to use this strategy :-( [\#53](https://github.com/auth0/omniauth-auth0/issues/53) | |
89 | - CSRF detected [\#49](https://github.com/auth0/omniauth-auth0/issues/49) | |
90 | - /auth/:provider route not registered? [\#47](https://github.com/auth0/omniauth-auth0/issues/47) | |
91 | ||
92 | **Added** | |
93 | - Add ID token validation [\#62](https://github.com/auth0/omniauth-auth0/pull/62) ([joshcanhelp](https://github.com/joshcanhelp)) | |
94 | - Silent authentication [\#59](https://github.com/auth0/omniauth-auth0/pull/59) ([batalla3692](https://github.com/batalla3692)) | |
95 | - Pass connection parameter to auth0 [\#54](https://github.com/auth0/omniauth-auth0/pull/54) ([tomgi](https://github.com/tomgi)) | |
96 | ||
97 | **Changed** | |
98 | - Update to omniauth-oauth2 [\#55](https://github.com/auth0/omniauth-auth0/pull/55) ([chills42](https://github.com/chills42)) | |
99 | ||
100 | **Fixed** | |
101 | - Fix Rubocop errors [\#66](https://github.com/auth0/omniauth-auth0/pull/66) ([joshcanhelp](https://github.com/joshcanhelp)) | |
102 | - Fix minute bug in README.md [\#63](https://github.com/auth0/omniauth-auth0/pull/63) ([rahuldess](https://github.com/rahuldess)) | |
1 | 103 | |
2 | 104 | ## [v2.0.0](https://github.com/auth0/omniauth-auth0/tree/v2.0.0) (2017-01-25) |
3 | 105 | [Full Changelog](https://github.com/auth0/omniauth-auth0/compare/v1.4.1...v2.0.0) |
4 | 106 | |
5 | 107 | Updated library to handle OIDC conformant clients and OAuth2 features in Auth0. |
6 | This affects how the `credentials` and `info` attributes are populated since the payload of /oauth/token and /userinfo are differnt when using OAuth2/OIDC features. | |
108 | This affects how the `credentials` and `info` attributes are populated since the payload of /oauth/token and /userinfo are different when using OAuth2/OIDC features. | |
7 | 109 | |
8 | 110 | The `credentials` hash will always have an `access_token` and might have a `refresh_token` (if it's allowed in your API settings in Auth0 dashboard and requested using `offline_access` scope) and an `id_token` (scope `openid` is needed for Auth0 to return it). |
9 | 111 | |
15 | 117 | - image: `picture` attribute in userinfo response. |
16 | 118 | |
17 | 119 | Also in `extra` will have in `raw_info` the full /userinfo response. |
120 | ||
121 | **Fixed** | |
122 | - Use image attribute of omniauth instead of picture [\#45](https://github.com/auth0/omniauth-auth0/pull/45) ([hzalaz](https://github.com/hzalaz)) | |
123 | - Rework strategy to handle OAuth and OIDC [\#44](https://github.com/auth0/omniauth-auth0/pull/44) ([hzalaz](https://github.com/hzalaz)) | |
124 | - lock v10 update, dependencies update [\#41](https://github.com/auth0/omniauth-auth0/pull/41) ([Amialc](https://github.com/Amialc)) | |
125 | ||
126 | ## [v1.4.2](https://github.com/auth0/omniauth-auth0/tree/v1.4.2) (2016-06-13) | |
127 | [Full Changelog](https://github.com/auth0/omniauth-auth0/compare/v1.4.1...v1.4.2) | |
128 | ||
129 | **Added** | |
130 | - Link to OmniAuth site [\#36](https://github.com/auth0/omniauth-auth0/pull/36) ([jghaines](https://github.com/jghaines)) | |
131 | - add ssl fix to RoR example [\#31](https://github.com/auth0/omniauth-auth0/pull/31) ([Amialc](https://github.com/Amialc)) | |
132 | - Update LICENSE [\#17](https://github.com/auth0/omniauth-auth0/pull/17) ([aguerere](https://github.com/aguerere)) | |
133 | ||
134 | **Changed** | |
135 | - Update lock to version 9 [\#34](https://github.com/auth0/omniauth-auth0/pull/34) ([Annyv2](https://github.com/Annyv2)) | |
136 | - Update Gemfile [\#22](https://github.com/auth0/omniauth-auth0/pull/22) ([Annyv2](https://github.com/Annyv2)) | |
137 | - Update lock [\#15](https://github.com/auth0/omniauth-auth0/pull/15) ([Annyv2](https://github.com/Annyv2)) | |
138 | ||
139 | **Fixed** | |
140 | - Fix setup [\#38](https://github.com/auth0/omniauth-auth0/pull/38) ([deepak](https://github.com/deepak)) | |
141 | - Added missing instruction [\#30](https://github.com/auth0/omniauth-auth0/pull/30) ([Annyv2](https://github.com/Annyv2)) | |
142 | - Fixes undefined Auth0Lock issue [\#28](https://github.com/auth0/omniauth-auth0/pull/28) ([Annyv2](https://github.com/Annyv2)) | |
143 | - Update Readme [\#27](https://github.com/auth0/omniauth-auth0/pull/27) ([Annyv2](https://github.com/Annyv2)) | |
144 | ||
18 | 145 | |
19 | 146 | ## [v1.4.1](https://github.com/auth0/omniauth-auth0/tree/v1.4.1) (2015-11-18) |
20 | 147 | [Full Changelog](https://github.com/auth0/omniauth-auth0/compare/v1.4.0...v1.4.1) |
0 | # Code of Conduct | |
1 | ||
2 | Please see [Auth0's Code of Conduct](https://github.com/auth0/open-source-template/blob/master/CODE-OF-CONDUCT.md) for information on contributing to this repo. |
0 | # Contribution | |
1 | ||
2 | **Thank you in advance for your contribution!** | |
3 | ||
4 | Please read [Auth0's contribution guidelines](https://github.com/auth0/open-source-template/blob/master/GENERAL-CONTRIBUTING.md) before beginning work on your contribution here. | |
5 | ||
6 | ## Environment setup | |
7 | ||
8 | The best way we've found to develop gems locally is by using a local setting for your Bundler config. First, checkout the project locally: | |
9 | ||
10 | ```bash | |
11 | $ pwd | |
12 | /PROJECT_ROOT/ | |
13 | $ mkdir vendor # if one does not exist | |
14 | $ echo "/vendor/" >> .gitignore | |
15 | $ git clone git@github.com:auth0/omniauth-auth0.git vendor/omniauth-auth0 | |
16 | Cloning into 'vendor/omniauth-auth0'... | |
17 | ``` | |
18 | ||
19 | Now, run the following command in your project root directory: | |
20 | ||
21 | ```bash | |
22 | $ bundle config --local local.omniauth-auth0 /PROJECT_ROOT/vendor/omniauth-auth0 | |
23 | You are replacing the current local value of local.omniauth-auth0, which is currently nil | |
24 | $ bundle config | |
25 | Settings are listed in order of priority. The top value will be used. | |
26 | local.omniauth-auth0 | |
27 | Set for your local app (/PROJECT_ROOT/.bundle/config): "/PROJECT_ROOT/vendor/omniauth-auth0" | |
28 | ``` | |
29 | ||
30 | Finally, add or change the gem include to add a `github:` param: | |
31 | ||
32 | ```ruby | |
33 | source 'https://rubygems.org' | |
34 | # ... | |
35 | # OmniAuth strategy for authenticating with Auth0 | |
36 | gem 'omniauth-auth0', github: 'auth0/omniauth-auth0' | |
37 | #.. | |
38 | ``` | |
39 | ||
40 | Now you should be able to make changes locally and have them reflected in your test app. Keep in mind you'll need to restart your app between changes. | |
41 | ||
42 | [Great explanation for why this setup works well](https://rossta.net/blog/how-to-specify-local-ruby-gems-in-your-gemfile.html). | |
43 | ||
44 | ## Testing | |
45 | ||
46 | Tests should be added for additional or modified functionality and all tests should run successfully before submitting a PR. | |
47 | ||
48 | ### Adding tests | |
49 | ||
50 | All new tests should be added to the `/spec/omniauth` directory. Testing resources, like JSON fixtures, should be added to the `/spec/resources` directory. | |
51 | ||
52 | ### Running tests | |
53 | ||
54 | Running tests is as simple as: | |
55 | ||
56 | ```bash | |
57 | $ bundle exec rake spec | |
58 | ``` | |
59 | ||
60 | ## Documentation | |
61 | ||
62 | Documentation for this gem is primarily done at the code level. All new methods should include a docblock at least. | |
63 | ||
64 | ## Code quality tools | |
65 | ||
66 | Code quality is enforced across the entire gem with Rubocop: | |
67 | ||
68 | ```bash | |
69 | $ bundle exec rake rubocop | |
70 | ``` |
0 | source 'http://rubygems.org' | |
0 | source 'https://rubygems.org' | |
1 | 1 | |
2 | 2 | gemspec |
3 | 3 | |
4 | 4 | gem 'gem-release' |
5 | gem 'jwt' | |
5 | 6 | gem 'rake' |
6 | 7 | |
7 | 8 | group :development do |
8 | 9 | gem 'dotenv' |
9 | 10 | gem 'pry' |
11 | gem 'rubocop', require: false | |
10 | 12 | gem 'shotgun' |
11 | 13 | gem 'sinatra' |
12 | 14 | gem 'thin' |
14 | 16 | |
15 | 17 | group :test do |
16 | 18 | gem 'guard-rspec', require: false |
17 | gem 'listen', '~> 3.1.5' | |
19 | gem 'listen', '~> 3' | |
18 | 20 | gem 'rack-test' |
19 | 21 | gem 'rspec', '~> 3.5' |
20 | gem 'rubocop', '>= 0.30', platforms: [ | |
21 | :ruby_19, :ruby_20, :ruby_21, :ruby_22 | |
22 | ] | |
22 | gem 'codecov', require: false | |
23 | 23 | gem 'simplecov' |
24 | 24 | gem 'webmock' |
25 | 25 | end |
0 | [![Build Status](https://travis-ci.org/auth0/omniauth-auth0.svg)](https://travis-ci.org/auth0/omniauth-auth0) | |
1 | ||
2 | 0 | # OmniAuth Auth0 |
3 | 1 | |
4 | This is the official [OmniAuth](https://github.com/intridea/omniauth) strategy for authenticating to [Auth0](https://auth0.com). | |
5 | ||
6 | ## Installing | |
7 | ||
8 | Add to your `Gemfile`: | |
2 | An [OmniAuth](https://github.com/intridea/omniauth) strategy for authenticating with [Auth0](https://auth0.com). This strategy is based on the [OmniAuth OAuth2](https://github.com/omniauth/omniauth-oauth2) strategy. | |
3 | ||
4 | > :warning: **Important security note:** This solution uses a 3rd party library with an unresolved [security issue(s)](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2015-9284). Please review the details of the vulnerability, including [Auth0](https://github.com/auth0/omniauth-auth0/issues/82 ) and other recommended [mitigations](https://github.com/omniauth/omniauth/wiki/Resolving-CVE-2015-9284), before implementing the solution. | |
5 | ||
6 | [![CircleCI](https://img.shields.io/circleci/project/github/auth0/omniauth-auth0/master.svg)](https://circleci.com/gh/auth0/omniauth-auth0) | |
7 | [![codecov](https://codecov.io/gh/auth0/omniauth-auth0/branch/master/graph/badge.svg)](https://codecov.io/gh/auth0/omniauth-auth0) | |
8 | [![Gem Version](https://badge.fury.io/rb/omniauth-auth0.svg)](https://badge.fury.io/rb/omniauth-auth0) | |
9 | [![MIT licensed](https://img.shields.io/dub/l/vibe-d.svg?style=flat)](https://github.com/auth0/omniauth-auth0/blob/master/LICENSE) | |
10 | [![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fauth0%2Fomniauth-auth0.svg?type=shield)](https://app.fossa.com/projects/git%2Bgithub.com%2Fauth0%2Fomniauth-auth0?ref=badge_shield) | |
11 | ||
12 | ## Table of Contents | |
13 | ||
14 | - [Documentation](#documentation) | |
15 | - [Installation](#installation) | |
16 | - [Getting Started](#getting-started) | |
17 | - [Contribution](#contribution) | |
18 | - [Support + Feedback](#support--feedback) | |
19 | - [Vulnerability Reporting](#vulnerability-reporting) | |
20 | - [What is Auth0](#what-is-auth0) | |
21 | - [License](#license) | |
22 | ||
23 | ## Documentation | |
24 | ||
25 | - [Ruby on Rails Quickstart](https://auth0.com/docs/quickstart/webapp/rails) | |
26 | - [Sample projects](https://github.com/auth0-samples/auth0-rubyonrails-sample) | |
27 | - [API Reference](https://www.rubydoc.info/gems/omniauth-auth0) | |
28 | ||
29 | ## Installation | |
30 | ||
31 | Add the following line to your `Gemfile`: | |
9 | 32 | |
10 | 33 | ```ruby |
11 | 34 | gem 'omniauth-auth0' |
12 | 35 | ``` |
13 | 36 | |
14 | Then `bundle install`. | |
15 | ||
16 | ## Usage | |
17 | ||
18 | ### Rails | |
19 | ||
20 | ```ruby | |
21 | Rails.application.config.middleware.use OmniAuth::Builder do | |
22 | provider :auth0, ENV['AUTH0_CLIENT_ID'], ENV['AUTH0_CLIENT_SECRET'], ENV['AUTH0_DOMAIN'] | |
23 | end | |
24 | ``` | |
25 | ||
26 | Then to redirect to your tenant's hosted login page: | |
27 | ||
28 | ```ruby | |
29 | redirect_to '/auth/auth0' | |
30 | ``` | |
31 | ||
32 | ### Sinatra | |
33 | ||
34 | ```ruby | |
35 | use OmniAuth::Builder do | |
36 | provider :auth0, ENV['AUTH0_CLIENT_ID'], ENV['AUTH0_CLIENT_SECRET'], ENV['AUTH0_DOMAIN'] | |
37 | end | |
38 | ``` | |
39 | ||
40 | Then to redirect to your tenant's hosted login page: | |
41 | ||
42 | ```ruby | |
43 | redirect to('/auth/auth0') | |
44 | ``` | |
45 | ||
46 | > You can customize your hosted login page in your [Auth0 Dashboard](https://manage.auth0.com/#/login_page) | |
47 | ||
48 | ### Auth parameters | |
49 | ||
50 | To send additional parameters during login you can specify them when you register the provider | |
51 | ||
52 | ```ruby | |
53 | provider | |
37 | If you're using this strategy with Rails, also add the following for CSRF protection: | |
38 | ||
39 | ```ruby | |
40 | gem 'omniauth-rails_csrf_protection' | |
41 | ``` | |
42 | ||
43 | Then install: | |
44 | ||
45 | ```bash | |
46 | $ bundle install | |
47 | ``` | |
48 | ||
49 | See our [contributing guide](CONTRIBUTING.md) for information on local installation for development. | |
50 | ||
51 | ## Getting Started | |
52 | ||
53 | To start processing authentication requests, the following steps must be performed: | |
54 | ||
55 | 1. Initialize the strategy | |
56 | 2. Configure the callback controller | |
57 | 3. Add the required routes | |
58 | 4. Trigger an authentication request | |
59 | ||
60 | All of these tasks and more are covered in our [Ruby on Rails Quickstart](https://auth0.com/docs/quickstart/webapp/rails). | |
61 | ||
62 | ### Additional authentication parameters | |
63 | ||
64 | To send additional parameters during login, you can specify them when you register the provider: | |
65 | ||
66 | ```ruby | |
67 | provider | |
54 | 68 | :auth0, |
55 | 69 | ENV['AUTH0_CLIENT_ID'], |
56 | 70 | ENV['AUTH0_CLIENT_SECRET'], |
58 | 72 | { |
59 | 73 | authorize_params: { |
60 | 74 | scope: 'openid read:users write:order', |
61 | audience: 'https://mydomain/api' | |
75 | audience: 'https://mydomain/api', | |
76 | max_age: 3600 # time in seconds authentication is valid | |
62 | 77 | } |
63 | 78 | } |
64 | 79 | ``` |
65 | 80 | |
66 | that will tell it to send those parameters on every Auth request. | |
67 | ||
68 | Or you can do it for a specific Auth request by adding them in the query parameter of the redirect url: | |
69 | ||
70 | ```ruby | |
71 | redirect_to '/auth/auth0?connection=google-oauth2' | |
72 | ``` | |
73 | ||
74 | ### Auth Hash | |
75 | ||
76 | Auth0 strategy will have the standard OmniAuth hash attributes: | |
77 | ||
78 | - provider: the name of the strategy, in this case `auth0` | |
79 | - uid: the user identifier | |
80 | - info: the result of the call to /userinfo using OmniAuth standard attributes | |
81 | - credentials: Auth0 tokens, at least will have an access_token but can eventually have refresh_token and/or id_token | |
82 | - extra: Additional info obtained from calling /userinfo in the attribute `raw_info` | |
83 | ||
84 | ```ruby | |
85 | { | |
86 | :provider => 'auth0', | |
87 | :uid => 'google-oauth2|this-is-the-google-id', | |
88 | :info => { | |
89 | :name => 'John Foo', | |
90 | :email => 'johnfoo@example.org', | |
91 | :nickname => 'john', | |
92 | :image => 'https://example.org/john.jpg' | |
93 | }, | |
94 | :credentials => { | |
95 | :token => 'XdDadllcas2134rdfdsI', | |
96 | :expires_at => 1485373937, | |
97 | :expires => true, | |
98 | :refresh_token => 'aKNajdjfj123nBasd', | |
99 | :id_token => 'eyJhbGciOiJIUzI1NiIsImN0eSI6IkpXVCJ9.eyJuYW1lIjoiSm9obiBGb28ifQ.lxAiy1rqve8ZHQEQVehUlP1sommPHVJDhgPgFPnDosg', | |
100 | :token_type => 'bearer', | |
101 | }, | |
102 | :extra => { | |
103 | :raw_info => { | |
104 | :email => 'johnfoo@example.org', | |
105 | :email_verified => 'true', | |
106 | :name => 'John Foo', | |
107 | :picture => 'https://example.org/john.jpg', | |
108 | :user_id => 'google-oauth2|this-is-the-google-id', | |
109 | :nickname => 'john', | |
110 | :created_at: '2014-07-15T17:19:50.387Z' | |
111 | } | |
112 | } | |
113 | } | |
114 | ``` | |
115 | ||
116 | ### ActionDispatch::Cookies::CookieOverflow issue | |
117 | ||
118 | If you are getting this error it means that you are using Cookie sessions and since you are storing the whole profile it overflows the max-size of 4K. | |
119 | ||
120 | You can change to use In-Memory store for development as follows: | |
121 | ||
122 | # /config/initializers/session_store.rb | |
123 | CrazyApp::Application.config.session_store :cache_store | |
124 | ||
125 | # /config/environments/development.rb | |
126 | config.cache_store = :memory_store | |
127 | ||
128 | ## Documentation | |
129 | ||
130 | For more information about [auth0](http://auth0.com) contact our [documentation page](http://docs.auth0.com/). | |
131 | ||
132 | ## Issue Reporting | |
133 | ||
134 | If you have found a bug or if you have a feature request, please report them at this repository issues section. Please do not report security vulnerabilities on the public GitHub issue tracker. The [Responsible Disclosure Program](https://auth0.com/whitehat) details the procedure for disclosing security issues. | |
135 | ||
136 | ## Author | |
137 | ||
138 | [Auth0](https://auth0.com) | |
81 | ... which will tell the strategy to send those parameters on every authentication request. | |
82 | ||
83 | ### Authentication hash | |
84 | ||
85 | The Auth0 strategy will provide the standard OmniAuth hash attributes: | |
86 | ||
87 | - `:provider` - the name of the strategy, in this case `auth0` | |
88 | - `:uid` - the user identifier | |
89 | - `:info` - the result of the call to `/userinfo` using OmniAuth standard attributes | |
90 | - `:credentials` - tokens requested and data | |
91 | - `:extra` - Additional info obtained from calling `/userinfo` in the `:raw_info` property | |
92 | ||
93 | ```ruby | |
94 | { | |
95 | :provider => 'auth0', | |
96 | :uid => 'auth0|USER_ID', | |
97 | :info => { | |
98 | :name => 'John Foo', | |
99 | :email => 'johnfoo@example.org', | |
100 | :nickname => 'john', | |
101 | :image => 'https://example.org/john.jpg' | |
102 | }, | |
103 | :credentials => { | |
104 | :token => 'ACCESS_TOKEN', | |
105 | :expires_at => 1485373937, | |
106 | :expires => true, | |
107 | :refresh_token => 'REFRESH_TOKEN', | |
108 | :id_token => 'JWT_ID_TOKEN', | |
109 | :token_type => 'bearer', | |
110 | }, | |
111 | :extra => { | |
112 | :raw_info => { | |
113 | :email => 'johnfoo@example.org', | |
114 | :email_verified => 'true', | |
115 | :name => 'John Foo', | |
116 | :picture => 'https://example.org/john.jpg', | |
117 | :user_id => 'auth0|USER_ID', | |
118 | :nickname => 'john', | |
119 | :created_at => '2014-07-15T17:19:50.387Z' | |
120 | } | |
121 | } | |
122 | } | |
123 | ``` | |
124 | ||
125 | ### Query Parameter Options | |
126 | ||
127 | In some scenarios, you may need to pass specific query parameters to `/authorize`. The following parameters are available to enable this: | |
128 | ||
129 | - `connection` | |
130 | - `connection_scope` | |
131 | - `prompt` | |
132 | - `screen_hint` (only relevant to New Universal Login Experience) | |
133 | - `organization` | |
134 | - `invitation` | |
135 | ||
136 | Simply pass these query parameters to your OmniAuth redirect endpoint to enable their behavior. | |
137 | ||
138 | ## Examples | |
139 | ||
140 | ### Auth0 Organizations (Closed Beta) | |
141 | ||
142 | Organizations is a set of features that provide better support for developers who build and maintain SaaS and Business-to-Business (B2B) applications. | |
143 | ||
144 | Using Organizations, you can: | |
145 | ||
146 | - Represent teams, business customers, partner companies, or any logical grouping of users that should have different ways of accessing your applications, as organizations. | |
147 | - Manage their membership in a variety of ways, including user invitation. | |
148 | - Configure branded, federated login flows for each organization. | |
149 | - Implement role-based access control, such that users can have different roles when authenticating in the context of different organizations. | |
150 | - Build administration capabilities into your products, using Organizations APIs, so that those businesses can manage their own organizations. | |
151 | ||
152 | Note that Organizations is currently only available to customers on our Enterprise and Startup subscription plans. | |
153 | ||
154 | #### Logging in with an Organization | |
155 | ||
156 | Logging in with an Organization is as easy as passing the parameters to the authorize endpoint. You can do this with | |
157 | ||
158 | ```ruby | |
159 | <%= | |
160 | button_to 'Login', 'auth/auth0', | |
161 | method: :post, | |
162 | params: { | |
163 | # Found in your Auth0 dashboard, under Organization settings: | |
164 | organization: '{AUTH0_ORGANIZATION}' | |
165 | } | |
166 | %> | |
167 | ``` | |
168 | ||
169 | Alternatively you can configure the organization when you register the provider: | |
170 | ||
171 | ```ruby | |
172 | provider | |
173 | :auth0, | |
174 | ENV['AUTH0_CLIENT_ID'], | |
175 | ENV['AUTH0_CLIENT_SECRET'], | |
176 | ENV['AUTH0_DOMAIN'], | |
177 | { | |
178 | authorize_params: { | |
179 | scope: 'openid read:users', | |
180 | audience: 'https://{AUTH0_DOMAIN}/api', | |
181 | organization: '{AUTH0_ORGANIZATION}' | |
182 | } | |
183 | } | |
184 | ``` | |
185 | ||
186 | #### Accepting user invitations | |
187 | ||
188 | Auth0 Organizations allow users to be invited using emailed links, which will direct a user back to your application. The URL the user will arrive at is based on your configured `Application Login URI`, which you can change from your Application's settings inside the Auth0 dashboard. | |
189 | ||
190 | When the user arrives at your application using an invite link, you can expect three query parameters to be provided: `invitation`, `organization`, and `organization_name`. These will always be delivered using a GET request. | |
191 | ||
192 | You can then supply those parametrs to a `button_to` or `link_to` helper | |
193 | ||
194 | ```ruby | |
195 | <%= | |
196 | button_to 'Login', 'auth/auth0', | |
197 | method: :post, | |
198 | params: { | |
199 | organization: '{YOUR_ORGANIZATION_ID}', | |
200 | invitation: '{INVITE_CODE}' | |
201 | } | |
202 | %> | |
203 | ``` | |
204 | ||
205 | ## Contribution | |
206 | ||
207 | We appreciate feedback and contribution to this repo! Before you get started, please see the following: | |
208 | ||
209 | - [Auth0's contribution guidelines](https://github.com/auth0/open-source-template/blob/master/GENERAL-CONTRIBUTING.md) | |
210 | - [Auth0's Code of Conduct](https://github.com/auth0/open-source-template/blob/master/CODE-OF-CONDUCT.md) | |
211 | - [This repo's contribution guide](CONTRIBUTING.md) | |
212 | ||
213 | ## Support + Feedback | |
214 | ||
215 | - Use [Community](https://community.auth0.com/) for usage, questions, specific cases. | |
216 | - Use [Issues](https://github.com/auth0/omniauth-auth0/issues) here for code-level support and bug reports. | |
217 | - Paid customers can use [Support](https://support.auth0.com/) to submit a trouble ticket for production-affecting issues. | |
218 | ||
219 | ## Vulnerability Reporting | |
220 | ||
221 | Please do not report security vulnerabilities on the public GitHub issue tracker. The [Responsible Disclosure Program](https://auth0.com/whitehat) details the procedure for disclosing security issues. | |
222 | ||
223 | ## What is Auth0? | |
224 | ||
225 | Auth0 helps you to easily: | |
226 | ||
227 | - implement authentication with multiple identity providers, including social (e.g., Google, Facebook, Microsoft, LinkedIn, GitHub, Twitter, etc), or enterprise (e.g., Windows Azure AD, Google Apps, Active Directory, ADFS, SAML, etc.) | |
228 | - log in users with username/password databases, passwordless, or multi-factor authentication | |
229 | - link multiple user accounts together | |
230 | - generate signed JSON Web Tokens to authorize your API calls and flow the user identity securely | |
231 | - access demographics and analytics detailing how, when, and where users are logging in | |
232 | - enrich user profiles from other data sources using customizable JavaScript rules | |
233 | ||
234 | [Why Auth0?](https://auth0.com/why-auth0) | |
139 | 235 | |
140 | 236 | ## License |
141 | 237 | |
142 | This project is licensed under the MIT license. See the [LICENSE](LICENSE) file for more info. | |
238 | The OmniAuth Auth0 strategy is licensed under MIT - [LICENSE](LICENSE) | |
239 | ||
240 | ||
241 | [![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fauth0%2Fomniauth-auth0.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.com%2Fauth0%2Fomniauth-auth0?ref=badge_large) |
9 | 9 | RuboCop::RakeTask.new |
10 | 10 | rescue LoadError |
11 | 11 | task :rubocop do |
12 | $stderr.puts 'Rubocop is disabled' | |
12 | warn 'Rubocop is disabled' | |
13 | 13 | end |
14 | 14 | end |
15 | 15 | |
22 | 22 | end |
23 | 23 | |
24 | 24 | desc 'Run specs' |
25 | task default: [:spec, :rubocop] | |
25 | task default: %i[spec rubocop] | |
26 | 26 | task test: :spec |
27 | 27 | task :guard do |
28 | 28 | system 'bundle exec guard' |
0 | coverage: | |
1 | precision: 2 | |
2 | round: down | |
3 | range: "60...100" | |
4 | status: | |
5 | project: | |
6 | default: | |
7 | enabled: true | |
8 | target: auto | |
9 | threshold: 5% | |
10 | if_no_uploads: error | |
11 | patch: | |
12 | default: | |
13 | enabled: true | |
14 | target: 80% | |
15 | threshold: 30% | |
16 | if_no_uploads: error | |
17 | changes: | |
18 | default: | |
19 | enabled: true | |
20 | if_no_uploads: error | |
21 | comment: false |
0 | module OmniAuth | |
1 | module Auth0 | |
2 | class TokenValidationError < StandardError | |
3 | attr_reader :error_reason | |
4 | def initialize(msg) | |
5 | @error_reason = msg | |
6 | super(msg) | |
7 | end | |
8 | end | |
9 | end | |
10 | end⏎ |
0 | require 'base64' | |
1 | require 'uri' | |
2 | require 'json' | |
3 | require 'omniauth' | |
4 | require 'omniauth/auth0/errors' | |
5 | ||
6 | module OmniAuth | |
7 | module Auth0 | |
8 | # JWT Validator class | |
9 | class JWTValidator | |
10 | attr_accessor :issuer, :domain | |
11 | ||
12 | # Initializer | |
13 | # @param options object | |
14 | # options.domain - Application domain. | |
15 | # options.issuer - Application issuer (optional). | |
16 | # options.client_id - Application Client ID. | |
17 | # options.client_secret - Application Client Secret. | |
18 | ||
19 | def initialize(options, authorize_params = {}) | |
20 | @domain = uri_string(options.domain) | |
21 | ||
22 | # Use custom issuer if provided, otherwise use domain | |
23 | @issuer = @domain | |
24 | @issuer = uri_string(options.issuer) if options.respond_to?(:issuer) | |
25 | ||
26 | @client_id = options.client_id | |
27 | @client_secret = options.client_secret | |
28 | end | |
29 | ||
30 | # Verify a token's signature. Only tokens signed with the RS256 or HS256 signatures are supported. | |
31 | # Deprecated: Please use `decode` instead | |
32 | # @return array - The token's key and signing algorithm | |
33 | def verify_signature(jwt) | |
34 | head = token_head(jwt) | |
35 | key, alg = extract_key(head) | |
36 | ||
37 | # Call decode to verify the signature | |
38 | JWT.decode(jwt, key, true, decode_opts(alg)) | |
39 | return key, alg | |
40 | end | |
41 | ||
42 | # Decodes a JWT and verifies it's signature. Only tokens signed with the RS256 or HS256 signatures are supported. | |
43 | # @param jwt string - JWT to verify. | |
44 | # @return hash - The decoded token, if there were no exceptions. | |
45 | # @see https://github.com/jwt/ruby-jwt | |
46 | def decode(jwt) | |
47 | head = token_head(jwt) | |
48 | key, alg = extract_key(head) | |
49 | ||
50 | # Call decode to verify the signature | |
51 | JWT.decode(jwt, key, true, decode_opts(alg)) | |
52 | end | |
53 | ||
54 | # Verify a JWT. | |
55 | # @param jwt string - JWT to verify. | |
56 | # @param authorize_params hash - Authorization params to verify on the JWT | |
57 | # @return hash - The verified token payload, if there were no exceptions. | |
58 | def verify(jwt, authorize_params = {}) | |
59 | if !jwt | |
60 | raise OmniAuth::Auth0::TokenValidationError.new('ID token is required but missing') | |
61 | end | |
62 | ||
63 | parts = jwt.split('.') | |
64 | if parts.length != 3 | |
65 | raise OmniAuth::Auth0::TokenValidationError.new('ID token could not be decoded') | |
66 | end | |
67 | ||
68 | id_token, header = decode(jwt) | |
69 | verify_claims(id_token, authorize_params) | |
70 | ||
71 | return id_token | |
72 | end | |
73 | ||
74 | # Get the decoded head segment from a JWT. | |
75 | # @return hash - The parsed head of the JWT passed, empty hash if not. | |
76 | def token_head(jwt) | |
77 | jwt_parts = jwt.split('.') | |
78 | return {} if blank?(jwt_parts) || blank?(jwt_parts[0]) | |
79 | ||
80 | json_parse(Base64.decode64(jwt_parts[0])) | |
81 | end | |
82 | ||
83 | # Get the JWKS from the issuer and return a public key. | |
84 | # @param x5c string - X.509 certificate chain from a JWKS. | |
85 | # @return key - The X.509 certificate public key. | |
86 | def jwks_public_cert(x5c) | |
87 | x5c = Base64.decode64(x5c) | |
88 | ||
89 | # https://docs.ruby-lang.org/en/2.4.0/OpenSSL/X509/Certificate.html | |
90 | OpenSSL::X509::Certificate.new(x5c).public_key | |
91 | end | |
92 | ||
93 | # Return a specific key from a JWKS object. | |
94 | # @param key string - Key to find in the JWKS. | |
95 | # @param kid string - Key ID to identify the right JWK. | |
96 | # @return nil|string | |
97 | def jwks_key(key, kid) | |
98 | return nil if blank?(jwks[:keys]) | |
99 | ||
100 | matching_jwk = jwks[:keys].find { |jwk| jwk[:kid] == kid } | |
101 | matching_jwk[key] if matching_jwk | |
102 | end | |
103 | ||
104 | private | |
105 | # Get the JWT decode options. We disable the claim checks since we perform our claim validation logic | |
106 | # Docs: https://github.com/jwt/ruby-jwt | |
107 | # @return hash | |
108 | def decode_opts(alg) | |
109 | { | |
110 | algorithm: alg, | |
111 | verify_expiration: false, | |
112 | verify_iat: false, | |
113 | verify_iss: false, | |
114 | verify_aud: false, | |
115 | verify_jti: false, | |
116 | verify_subj: false, | |
117 | verify_not_before: false | |
118 | } | |
119 | end | |
120 | ||
121 | def extract_key(head) | |
122 | if head[:alg] == 'RS256' | |
123 | key, alg = [rs256_decode_key(head[:kid]), head[:alg]] | |
124 | elsif head[:alg] == 'HS256' | |
125 | key, alg = [@client_secret, head[:alg]] | |
126 | else | |
127 | raise OmniAuth::Auth0::TokenValidationError.new("Signature algorithm of #{head[:alg]} is not supported. Expected the ID token to be signed with RS256 or HS256") | |
128 | end | |
129 | end | |
130 | ||
131 | def rs256_decode_key(kid) | |
132 | jwks_x5c = jwks_key(:x5c, kid) | |
133 | ||
134 | if jwks_x5c.nil? | |
135 | raise OmniAuth::Auth0::TokenValidationError.new("Could not find a public key for Key ID (kid) '#{kid}'") | |
136 | end | |
137 | ||
138 | jwks_public_cert(jwks_x5c.first) | |
139 | end | |
140 | ||
141 | # Get a JWKS from the domain | |
142 | # @return void | |
143 | def jwks | |
144 | jwks_uri = URI(@domain + '.well-known/jwks.json') | |
145 | @jwks ||= json_parse(Net::HTTP.get(jwks_uri)) | |
146 | end | |
147 | ||
148 | # Rails Active Support blank method. | |
149 | # @param obj object - Object to check for blankness. | |
150 | # @return boolean | |
151 | def blank?(obj) | |
152 | obj.respond_to?(:empty?) ? obj.empty? : !obj | |
153 | end | |
154 | ||
155 | # Parse JSON with symbolized names. | |
156 | # @param json string - JSON to parse. | |
157 | # @return hash | |
158 | def json_parse(json) | |
159 | JSON.parse(json, symbolize_names: true) | |
160 | end | |
161 | ||
162 | # Parse a URI into the desired string format | |
163 | # @param uri - the URI to parse | |
164 | # @return string | |
165 | def uri_string(uri) | |
166 | temp_domain = URI(uri) | |
167 | temp_domain = URI("https://#{uri}") unless temp_domain.scheme | |
168 | temp_domain = temp_domain.to_s | |
169 | temp_domain.end_with?('/') ? temp_domain : "#{temp_domain}/" | |
170 | end | |
171 | ||
172 | def verify_claims(id_token, authorize_params) | |
173 | leeway = authorize_params[:leeway] || 60 | |
174 | max_age = authorize_params[:max_age] | |
175 | nonce = authorize_params[:nonce] | |
176 | organization = authorize_params[:organization] | |
177 | ||
178 | verify_iss(id_token) | |
179 | verify_sub(id_token) | |
180 | verify_aud(id_token) | |
181 | verify_expiration(id_token, leeway) | |
182 | verify_iat(id_token) | |
183 | verify_nonce(id_token, nonce) | |
184 | verify_azp(id_token) | |
185 | verify_auth_time(id_token, leeway, max_age) | |
186 | verify_org(id_token, organization) | |
187 | end | |
188 | ||
189 | def verify_iss(id_token) | |
190 | issuer = id_token['iss'] | |
191 | if !issuer | |
192 | raise OmniAuth::Auth0::TokenValidationError.new("Issuer (iss) claim must be a string present in the ID token") | |
193 | elsif @issuer != issuer | |
194 | raise OmniAuth::Auth0::TokenValidationError.new("Issuer (iss) claim mismatch in the ID token, expected (#{@issuer}), found (#{id_token['iss']})") | |
195 | end | |
196 | end | |
197 | ||
198 | def verify_sub(id_token) | |
199 | subject = id_token['sub'] | |
200 | if !subject || !subject.is_a?(String) || subject.empty? | |
201 | raise OmniAuth::Auth0::TokenValidationError.new('Subject (sub) claim must be a string present in the ID token') | |
202 | end | |
203 | end | |
204 | ||
205 | def verify_aud(id_token) | |
206 | audience = id_token['aud'] | |
207 | if !audience || !(audience.is_a?(String) || audience.is_a?(Array)) | |
208 | raise OmniAuth::Auth0::TokenValidationError.new("Audience (aud) claim must be a string or array of strings present in the ID token") | |
209 | elsif audience.is_a?(Array) && !audience.include?(@client_id) | |
210 | raise OmniAuth::Auth0::TokenValidationError.new("Audience (aud) claim mismatch in the ID token; expected #{@client_id} but was not one of #{audience.join(', ')}") | |
211 | elsif audience.is_a?(String) && audience != @client_id | |
212 | raise OmniAuth::Auth0::TokenValidationError.new("Audience (aud) claim mismatch in the ID token; expected #{@client_id} but found #{audience}") | |
213 | end | |
214 | end | |
215 | ||
216 | def verify_expiration(id_token, leeway) | |
217 | expiration = id_token['exp'] | |
218 | if !expiration || !expiration.is_a?(Integer) | |
219 | raise OmniAuth::Auth0::TokenValidationError.new("Expiration time (exp) claim must be a number present in the ID token") | |
220 | elsif expiration <= Time.now.to_i - leeway | |
221 | raise OmniAuth::Auth0::TokenValidationError.new("Expiration time (exp) claim error in the ID token; current time (#{Time.now}) is after expiration time (#{Time.at(expiration + leeway)})") | |
222 | end | |
223 | end | |
224 | ||
225 | def verify_iat(id_token) | |
226 | if !id_token['iat'] | |
227 | raise OmniAuth::Auth0::TokenValidationError.new("Issued At (iat) claim must be a number present in the ID token") | |
228 | end | |
229 | end | |
230 | ||
231 | def verify_nonce(id_token, nonce) | |
232 | if nonce | |
233 | received_nonce = id_token['nonce'] | |
234 | if !received_nonce | |
235 | raise OmniAuth::Auth0::TokenValidationError.new("Nonce (nonce) claim must be a string present in the ID token") | |
236 | elsif nonce != received_nonce | |
237 | raise OmniAuth::Auth0::TokenValidationError.new("Nonce (nonce) claim value mismatch in the ID token; expected (#{nonce}), found (#{received_nonce})") | |
238 | end | |
239 | end | |
240 | end | |
241 | ||
242 | def verify_azp(id_token) | |
243 | audience = id_token['aud'] | |
244 | if audience.is_a?(Array) && audience.length > 1 | |
245 | azp = id_token['azp'] | |
246 | if !azp || !azp.is_a?(String) | |
247 | raise OmniAuth::Auth0::TokenValidationError.new("Authorized Party (azp) claim must be a string present in the ID token when Audience (aud) claim has multiple values") | |
248 | elsif azp != @client_id | |
249 | raise OmniAuth::Auth0::TokenValidationError.new("Authorized Party (azp) claim mismatch in the ID token; expected (#{@client_id}), found (#{azp})") | |
250 | end | |
251 | end | |
252 | end | |
253 | ||
254 | def verify_auth_time(id_token, leeway, max_age) | |
255 | if max_age | |
256 | auth_time = id_token['auth_time'] | |
257 | if !auth_time || !auth_time.is_a?(Integer) | |
258 | raise OmniAuth::Auth0::TokenValidationError.new("Authentication Time (auth_time) claim must be a number present in the ID token when Max Age (max_age) is specified") | |
259 | elsif Time.now.to_i > auth_time + max_age + leeway; | |
260 | raise OmniAuth::Auth0::TokenValidationError.new("Authentication Time (auth_time) claim in the ID token indicates that too much time has passed since the last end-user authentication. Current time (#{Time.now}) is after last auth time (#{Time.at(auth_time + max_age + leeway)})") | |
261 | end | |
262 | end | |
263 | end | |
264 | ||
265 | def verify_org(id_token, organization) | |
266 | if organization | |
267 | org_id = id_token['org_id'] | |
268 | if !org_id || !org_id.is_a?(String) | |
269 | raise OmniAuth::Auth0::TokenValidationError.new("Organization Id (org_id) claim must be a string present in the ID token") | |
270 | elsif org_id != organization | |
271 | raise OmniAuth::Auth0::TokenValidationError.new("Organization Id (org_id) claim value mismatch in the ID token; expected '#{organization}', found '#{org_id}'") | |
272 | end | |
273 | end | |
274 | end | |
275 | end | |
276 | end | |
277 | end |
0 | require 'json' | |
1 | ||
2 | module OmniAuth | |
3 | module Auth0 | |
4 | # Module to provide necessary telemetry for API requests. | |
5 | module Telemetry | |
6 | ||
7 | # Return a telemetry hash to be encoded and sent to Auth0. | |
8 | # @return hash | |
9 | def telemetry | |
10 | telemetry = { | |
11 | name: 'omniauth-auth0', | |
12 | version: OmniAuth::Auth0::VERSION, | |
13 | env: { | |
14 | ruby: RUBY_VERSION | |
15 | } | |
16 | } | |
17 | add_rails_version telemetry | |
18 | end | |
19 | ||
20 | # JSON-ify and base64 encode the current telemetry. | |
21 | # @return string | |
22 | def telemetry_encoded | |
23 | Base64.urlsafe_encode64(JSON.dump(telemetry)) | |
24 | end | |
25 | ||
26 | private | |
27 | ||
28 | def add_rails_version(telemetry) | |
29 | return telemetry unless Gem.loaded_specs['rails'].respond_to? :version | |
30 | telemetry[:env][:rails] = Gem.loaded_specs['rails'].version.to_s | |
31 | telemetry | |
32 | end | |
33 | end | |
34 | end | |
35 | end |
0 | # frozen_string_literal: true | |
1 | ||
0 | 2 | require 'base64' |
1 | 3 | require 'uri' |
4 | require 'securerandom' | |
2 | 5 | require 'omniauth-oauth2' |
6 | require 'omniauth/auth0/jwt_validator' | |
7 | require 'omniauth/auth0/telemetry' | |
8 | require 'omniauth/auth0/errors' | |
3 | 9 | |
4 | 10 | module OmniAuth |
5 | 11 | module Strategies |
6 | 12 | # Auth0 OmniAuth strategy |
7 | 13 | class Auth0 < OmniAuth::Strategies::OAuth2 |
14 | include OmniAuth::Auth0::Telemetry | |
15 | ||
8 | 16 | option :name, 'auth0' |
9 | 17 | |
10 | args [ | |
11 | :client_id, | |
12 | :client_secret, | |
13 | :domain | |
18 | args %i[ | |
19 | client_id | |
20 | client_secret | |
21 | domain | |
14 | 22 | ] |
15 | 23 | |
24 | # Setup client URLs used during authentication | |
16 | 25 | def client |
17 | 26 | options.client_options.site = domain_url |
18 | 27 | options.client_options.authorize_url = '/authorize' |
21 | 30 | super |
22 | 31 | end |
23 | 32 | |
33 | # Use the "sub" key of the userinfo returned | |
34 | # as the uid (globally unique string identifier). | |
24 | 35 | uid { raw_info['sub'] } |
25 | 36 | |
37 | # Build the API credentials hash with returned auth data. | |
26 | 38 | credentials do |
27 | hash = { 'token' => access_token.token } | |
28 | hash['expires'] = true | |
39 | credentials = { | |
40 | 'token' => access_token.token, | |
41 | 'expires' => true | |
42 | } | |
43 | ||
29 | 44 | if access_token.params |
30 | hash['id_token'] = access_token.params['id_token'] | |
31 | hash['token_type'] = access_token.params['token_type'] | |
32 | hash['refresh_token'] = access_token.refresh_token | |
45 | credentials.merge!( | |
46 | 'id_token' => access_token.params['id_token'], | |
47 | 'token_type' => access_token.params['token_type'], | |
48 | 'refresh_token' => access_token.refresh_token | |
49 | ) | |
33 | 50 | end |
34 | hash | |
51 | ||
52 | # Retrieve and remove authorization params from the session | |
53 | session_authorize_params = session['authorize_params'] || {} | |
54 | session.delete('authorize_params') | |
55 | ||
56 | auth_scope = session_authorize_params[:scope] | |
57 | if auth_scope.respond_to?(:include?) && auth_scope.include?('openid') | |
58 | # Make sure the ID token can be verified and decoded. | |
59 | jwt_validator.verify(credentials['id_token'], session_authorize_params) | |
60 | end | |
61 | ||
62 | credentials | |
35 | 63 | end |
36 | 64 | |
65 | # Store all raw information for use in the session. | |
37 | 66 | extra do |
38 | 67 | { |
39 | 68 | raw_info: raw_info |
40 | 69 | } |
41 | 70 | end |
42 | 71 | |
72 | # Build a hash of information about the user | |
73 | # with keys taken from the Auth Hash Schema. | |
43 | 74 | info do |
44 | 75 | { |
45 | 76 | name: raw_info['name'] || raw_info['sub'], |
49 | 80 | } |
50 | 81 | end |
51 | 82 | |
83 | # Define the parameters used for the /authorize endpoint | |
52 | 84 | def authorize_params |
53 | 85 | params = super |
54 | params['auth0Client'] = client_info | |
86 | %w[connection connection_scope prompt screen_hint login_hint organization invitation].each do |key| | |
87 | params[key] = request.params[key] if request.params.key?(key) | |
88 | end | |
89 | ||
90 | # Generate nonce | |
91 | params[:nonce] = SecureRandom.hex | |
92 | # Generate leeway if none exists | |
93 | params[:leeway] = 60 unless params[:leeway] | |
94 | ||
95 | # Store authorize params in the session for token verification | |
96 | session['authorize_params'] = params | |
97 | ||
55 | 98 | params |
56 | 99 | end |
57 | 100 | |
101 | def build_access_token | |
102 | options.token_params[:headers] = { 'Auth0-Client' => telemetry_encoded } | |
103 | super | |
104 | end | |
105 | ||
106 | # Declarative override for the request phase of authentication | |
58 | 107 | def request_phase |
59 | 108 | if no_client_id? |
109 | # Do we have a client_id for this Application? | |
60 | 110 | fail!(:missing_client_id) |
61 | 111 | elsif no_client_secret? |
112 | # Do we have a client_secret for this Application? | |
62 | 113 | fail!(:missing_client_secret) |
63 | 114 | elsif no_domain? |
115 | # Do we have a domain for this Application? | |
64 | 116 | fail!(:missing_domain) |
65 | 117 | else |
118 | # All checks pass, run the Oauth2 request_phase method. | |
66 | 119 | super |
67 | 120 | end |
68 | 121 | end |
69 | 122 | |
70 | private | |
71 | ||
72 | def raw_info | |
73 | userinfo_url = options.client_options.userinfo_url | |
74 | @raw_info ||= access_token.get(userinfo_url).parsed | |
123 | def callback_phase | |
124 | super | |
125 | rescue OmniAuth::Auth0::TokenValidationError => e | |
126 | fail!(:token_validation_error, e) | |
75 | 127 | end |
76 | 128 | |
129 | private | |
130 | def jwt_validator | |
131 | @jwt_validator ||= OmniAuth::Auth0::JWTValidator.new(options) | |
132 | end | |
133 | ||
134 | # Parse the raw user info. | |
135 | def raw_info | |
136 | return @raw_info if @raw_info | |
137 | ||
138 | if access_token["id_token"] | |
139 | claims, header = jwt_validator.decode(access_token["id_token"]) | |
140 | @raw_info = claims | |
141 | else | |
142 | userinfo_url = options.client_options.userinfo_url | |
143 | @raw_info = access_token.get(userinfo_url).parsed | |
144 | end | |
145 | ||
146 | return @raw_info | |
147 | end | |
148 | ||
149 | # Check if the options include a client_id | |
77 | 150 | def no_client_id? |
78 | 151 | ['', nil].include?(options.client_id) |
79 | 152 | end |
80 | 153 | |
154 | # Check if the options include a client_secret | |
81 | 155 | def no_client_secret? |
82 | 156 | ['', nil].include?(options.client_secret) |
83 | 157 | end |
84 | 158 | |
159 | # Check if the options include a domain | |
85 | 160 | def no_domain? |
86 | 161 | ['', nil].include?(options.domain) |
87 | 162 | end |
88 | 163 | |
164 | # Normalize a domain to a URL. | |
89 | 165 | def domain_url |
90 | 166 | domain_url = URI(options.domain) |
91 | 167 | domain_url = URI("https://#{domain_url}") if domain_url.scheme.nil? |
92 | 168 | domain_url.to_s |
93 | 169 | end |
94 | ||
95 | def client_info | |
96 | client_info = JSON.dump( | |
97 | name: 'omniauth-auth0', | |
98 | version: OmniAuth::Auth0::VERSION | |
99 | ) | |
100 | Base64.urlsafe_encode64(client_info) | |
101 | end | |
102 | 170 | end |
103 | 171 | end |
104 | 172 | end |
0 | 0 | module OmniAuth |
1 | 1 | module Auth0 |
2 | VERSION = '2.0.0'.freeze | |
2 | VERSION = '2.6.0'.freeze | |
3 | 3 | end |
4 | 4 | end |
0 | require 'omniauth-auth0/version' # rubocop:disable Style/FileName | |
0 | require 'omniauth-auth0/version' | |
1 | 1 | require 'omniauth/strategies/auth0' |
7 | 7 | s.authors = ['Auth0'] |
8 | 8 | s.email = ['info@auth0.com'] |
9 | 9 | s.homepage = 'https://github.com/auth0/omniauth-auth0' |
10 | s.summary = 'Omniauth OAuth2 strategy for the Auth0 platform.' | |
10 | s.summary = 'OmniAuth OAuth2 strategy for the Auth0 platform.' | |
11 | 11 | s.description = %q{Auth0 is an authentication broker that supports social identity providers as well as enterprise identity providers such as Active Directory, LDAP, Google Apps, Salesforce. |
12 | 12 | |
13 | 13 | OmniAuth is a library that standardizes multi-provider authentication for web applications. It was created to be powerful, flexible, and do as little as possible. |
14 | 14 | |
15 | omniauth-auth0 is the omniauth strategy for Auth0. | |
15 | omniauth-auth0 is the OmniAuth strategy for Auth0. | |
16 | 16 | } |
17 | ||
18 | s.rubyforge_project = 'omniauth-auth0' | |
19 | 17 | |
20 | 18 | s.files = `git ls-files`.split("\n") |
21 | 19 | s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") |
22 | 20 | s.executables = `git ls-files -- bin/*`.split('\n').map{ |f| File.basename(f) } |
23 | 21 | s.require_paths = ['lib'] |
24 | 22 | |
25 | s.add_runtime_dependency 'omniauth-oauth2', '~> 1.4' | |
23 | s.add_runtime_dependency 'omniauth', '~> 1.9' | |
24 | s.add_runtime_dependency 'omniauth-oauth2', '~> 1.5' | |
26 | 25 | |
27 | s.add_development_dependency 'bundler', '~> 1.9' | |
26 | s.add_development_dependency 'bundler' | |
28 | 27 | |
29 | 28 | s.license = 'MIT' |
30 | 29 | end |
0 | require 'spec_helper' | |
1 | require 'json' | |
2 | require 'jwt' | |
3 | ||
4 | describe OmniAuth::Auth0::JWTValidator do | |
5 | # | |
6 | # Reused data | |
7 | # | |
8 | ||
9 | let(:client_id) { 'CLIENT_ID' } | |
10 | let(:client_secret) { 'CLIENT_SECRET' } | |
11 | let(:domain) { 'samples.auth0.com' } | |
12 | let(:future_timecode) { 32_503_680_000 } | |
13 | let(:past_timecode) { 303_912_000 } | |
14 | let(:valid_jwks_kid) { 'NkJCQzIyQzRBMEU4NjhGNUU4MzU4RkY0M0ZDQzkwOUQ0Q0VGNUMwQg' } | |
15 | ||
16 | let(:rsa_private_key) do | |
17 | OpenSSL::PKey::RSA.generate 2048 | |
18 | end | |
19 | ||
20 | let(:valid_jwks) do | |
21 | { | |
22 | keys: [ | |
23 | { | |
24 | kid: valid_jwks_kid, | |
25 | x5c: [Base64.encode64(make_cert(rsa_private_key).to_der)] | |
26 | } | |
27 | ] | |
28 | }.to_json | |
29 | end | |
30 | ||
31 | let(:jwks) do | |
32 | current_dir = File.dirname(__FILE__) | |
33 | jwks_file = File.read("#{current_dir}/../../resources/jwks.json") | |
34 | JSON.parse(jwks_file, symbolize_names: true) | |
35 | end | |
36 | ||
37 | # | |
38 | # Specs | |
39 | # | |
40 | ||
41 | describe 'JWT verifier default values' do | |
42 | let(:jwt_validator) do | |
43 | make_jwt_validator | |
44 | end | |
45 | ||
46 | it 'should have the correct issuer' do | |
47 | expect(jwt_validator.issuer).to eq('https://samples.auth0.com/') | |
48 | end | |
49 | end | |
50 | ||
51 | describe 'JWT verifier token_head' do | |
52 | let(:jwt_validator) do | |
53 | make_jwt_validator | |
54 | end | |
55 | ||
56 | it 'should parse the head of a valid JWT' do | |
57 | expect(jwt_validator.token_head(make_hs256_token)[:alg]).to eq('HS256') | |
58 | end | |
59 | ||
60 | it 'should fail parsing the head of a blank JWT' do | |
61 | expect(jwt_validator.token_head('')).to eq({}) | |
62 | end | |
63 | ||
64 | it 'should fail parsing the head of an invalid JWT' do | |
65 | expect(jwt_validator.token_head('.')).to eq({}) | |
66 | end | |
67 | ||
68 | it 'should throw an exception for invalid JSON' do | |
69 | expect do | |
70 | jwt_validator.token_head('QXV0aDA=') | |
71 | end.to raise_error(JSON::ParserError) | |
72 | end | |
73 | end | |
74 | ||
75 | describe 'JWT verifier jwks_public_cert' do | |
76 | let(:jwt_validator) do | |
77 | make_jwt_validator | |
78 | end | |
79 | ||
80 | it 'should return a public_key' do | |
81 | x5c = jwks[:keys].first[:x5c].first | |
82 | public_cert = jwt_validator.jwks_public_cert(x5c) | |
83 | expect(public_cert.instance_of?(OpenSSL::PKey::RSA)).to eq(true) | |
84 | end | |
85 | ||
86 | it 'should fail with an invalid x5c' do | |
87 | expect do | |
88 | jwt_validator.jwks_public_cert('QXV0aDA=') | |
89 | end.to raise_error(OpenSSL::X509::CertificateError) | |
90 | end | |
91 | end | |
92 | ||
93 | describe 'JWT verifier jwks key parsing' do | |
94 | let(:jwt_validator) do | |
95 | make_jwt_validator | |
96 | end | |
97 | ||
98 | before do | |
99 | stub_complete_jwks | |
100 | end | |
101 | ||
102 | it 'should return a key' do | |
103 | expect(jwt_validator.jwks_key(:alg, valid_jwks_kid)).to eq('RS256') | |
104 | end | |
105 | ||
106 | it 'should return an x5c key' do | |
107 | expect(jwt_validator.jwks_key(:x5c, valid_jwks_kid).length).to eq(1) | |
108 | end | |
109 | ||
110 | it 'should return nil if there is not key' do | |
111 | expect(jwt_validator.jwks_key(:auth0, valid_jwks_kid)).to eq(nil) | |
112 | end | |
113 | ||
114 | it 'should return nil if the key ID is invalid' do | |
115 | expect(jwt_validator.jwks_key(:alg, "#{valid_jwks_kid}_invalid")).to eq(nil) | |
116 | end | |
117 | end | |
118 | ||
119 | describe 'JWT verifier custom issuer' do | |
120 | context 'same as domain' do | |
121 | let(:jwt_validator) do | |
122 | make_jwt_validator(opt_issuer: domain) | |
123 | end | |
124 | ||
125 | it 'should have the correct issuer' do | |
126 | expect(jwt_validator.issuer).to eq('https://samples.auth0.com/') | |
127 | end | |
128 | ||
129 | it 'should have the correct domain' do | |
130 | expect(jwt_validator.issuer).to eq('https://samples.auth0.com/') | |
131 | end | |
132 | end | |
133 | ||
134 | context 'different from domain' do | |
135 | shared_examples_for 'has correct issuer and domain' do | |
136 | let(:jwt_validator) { make_jwt_validator(opt_issuer: opt_issuer) } | |
137 | ||
138 | it 'should have the correct issuer' do | |
139 | expect(jwt_validator.issuer).to eq('https://different.auth0.com/') | |
140 | end | |
141 | ||
142 | it 'should have the correct domain' do | |
143 | expect(jwt_validator.domain).to eq('https://samples.auth0.com/') | |
144 | end | |
145 | end | |
146 | ||
147 | context 'without protocol and trailing slash' do | |
148 | let(:opt_issuer) { 'different.auth0.com' } | |
149 | it_behaves_like 'has correct issuer and domain' | |
150 | end | |
151 | ||
152 | context 'with protocol and trailing slash' do | |
153 | let(:opt_issuer) { 'https://different.auth0.com/' } | |
154 | it_behaves_like 'has correct issuer and domain' | |
155 | end | |
156 | end | |
157 | end | |
158 | ||
159 | describe 'JWT verifier verify' do | |
160 | let(:jwt_validator) do | |
161 | make_jwt_validator | |
162 | end | |
163 | ||
164 | before do | |
165 | stub_complete_jwks | |
166 | stub_expected_jwks | |
167 | end | |
168 | ||
169 | it 'should fail when JWT is nil' do | |
170 | expect do | |
171 | jwt_validator.verify(nil) | |
172 | end.to raise_error(an_instance_of(OmniAuth::Auth0::TokenValidationError).and having_attributes({ | |
173 | message: "ID token is required but missing" | |
174 | })) | |
175 | end | |
176 | ||
177 | it 'should fail when JWT is not well-formed' do | |
178 | expect do | |
179 | jwt_validator.verify('abc.123') | |
180 | end.to raise_error(an_instance_of(OmniAuth::Auth0::TokenValidationError).and having_attributes({ | |
181 | message: "ID token could not be decoded" | |
182 | })) | |
183 | end | |
184 | ||
185 | it 'should fail with missing issuer' do | |
186 | expect do | |
187 | jwt_validator.verify(make_hs256_token) | |
188 | end.to raise_error(an_instance_of(OmniAuth::Auth0::TokenValidationError).and having_attributes({ | |
189 | message: "Issuer (iss) claim must be a string present in the ID token" | |
190 | })) | |
191 | end | |
192 | ||
193 | it 'should fail with invalid issuer' do | |
194 | payload = { | |
195 | iss: 'https://auth0.com/' | |
196 | } | |
197 | token = make_hs256_token(payload) | |
198 | expect do | |
199 | jwt_validator.verify(token) | |
200 | end.to raise_error(an_instance_of(OmniAuth::Auth0::TokenValidationError).and having_attributes({ | |
201 | message: "Issuer (iss) claim mismatch in the ID token, expected (https://samples.auth0.com/), found (https://auth0.com/)" | |
202 | })) | |
203 | end | |
204 | ||
205 | it 'should fail when subject is missing' do | |
206 | payload = { | |
207 | iss: "https://#{domain}/", | |
208 | sub: '' | |
209 | } | |
210 | token = make_hs256_token(payload) | |
211 | expect do | |
212 | jwt_validator.verify(token) | |
213 | end.to raise_error(an_instance_of(OmniAuth::Auth0::TokenValidationError).and having_attributes({ | |
214 | message: "Subject (sub) claim must be a string present in the ID token" | |
215 | })) | |
216 | end | |
217 | ||
218 | it 'should fail with missing audience' do | |
219 | payload = { | |
220 | iss: "https://#{domain}/", | |
221 | sub: 'sub' | |
222 | } | |
223 | token = make_hs256_token(payload) | |
224 | expect do | |
225 | jwt_validator.verify(token) | |
226 | end.to raise_error(an_instance_of(OmniAuth::Auth0::TokenValidationError).and having_attributes({ | |
227 | message: "Audience (aud) claim must be a string or array of strings present in the ID token" | |
228 | })) | |
229 | end | |
230 | ||
231 | it 'should fail with invalid audience' do | |
232 | payload = { | |
233 | iss: "https://#{domain}/", | |
234 | sub: 'sub', | |
235 | aud: 'Auth0' | |
236 | } | |
237 | token = make_hs256_token(payload) | |
238 | expect do | |
239 | jwt_validator.verify(token) | |
240 | end.to raise_error(an_instance_of(OmniAuth::Auth0::TokenValidationError).and having_attributes({ | |
241 | message: "Audience (aud) claim mismatch in the ID token; expected #{client_id} but found Auth0" | |
242 | })) | |
243 | end | |
244 | ||
245 | it 'should fail when missing expiration' do | |
246 | payload = { | |
247 | iss: "https://#{domain}/", | |
248 | sub: 'sub', | |
249 | aud: client_id | |
250 | } | |
251 | ||
252 | token = make_hs256_token(payload) | |
253 | expect do | |
254 | jwt_validator.verify(token) | |
255 | end.to raise_error(an_instance_of(OmniAuth::Auth0::TokenValidationError).and having_attributes({ | |
256 | message: "Expiration time (exp) claim must be a number present in the ID token" | |
257 | })) | |
258 | end | |
259 | ||
260 | it 'should fail when past expiration' do | |
261 | payload = { | |
262 | iss: "https://#{domain}/", | |
263 | sub: 'sub', | |
264 | aud: client_id, | |
265 | exp: past_timecode | |
266 | } | |
267 | ||
268 | token = make_hs256_token(payload) | |
269 | expect do | |
270 | jwt_validator.verify(token) | |
271 | end.to raise_error(an_instance_of(OmniAuth::Auth0::TokenValidationError).and having_attributes({ | |
272 | message: "Expiration time (exp) claim error in the ID token; current time (#{Time.now}) is after expiration time (#{Time.at(past_timecode + 60)})" | |
273 | })) | |
274 | end | |
275 | ||
276 | it 'should pass when past expiration but within default leeway' do | |
277 | exp = Time.now.to_i - 59 | |
278 | payload = { | |
279 | iss: "https://#{domain}/", | |
280 | sub: 'sub', | |
281 | aud: client_id, | |
282 | exp: exp, | |
283 | iat: past_timecode | |
284 | } | |
285 | ||
286 | token = make_hs256_token(payload) | |
287 | id_token = jwt_validator.verify(token) | |
288 | expect(id_token['exp']).to eq(exp) | |
289 | end | |
290 | ||
291 | it 'should fail when past expiration and outside default leeway' do | |
292 | exp = Time.now.to_i - 61 | |
293 | payload = { | |
294 | iss: "https://#{domain}/", | |
295 | sub: 'sub', | |
296 | aud: client_id, | |
297 | exp: exp, | |
298 | iat: past_timecode | |
299 | } | |
300 | ||
301 | token = make_hs256_token(payload) | |
302 | expect do | |
303 | jwt_validator.verify(token) | |
304 | end.to raise_error(an_instance_of(OmniAuth::Auth0::TokenValidationError).and having_attributes({ | |
305 | message: "Expiration time (exp) claim error in the ID token; current time (#{Time.now}) is after expiration time (#{Time.at(exp + 60)})" | |
306 | })) | |
307 | end | |
308 | ||
309 | it 'should fail when missing iat' do | |
310 | payload = { | |
311 | iss: "https://#{domain}/", | |
312 | sub: 'sub', | |
313 | aud: client_id, | |
314 | exp: future_timecode | |
315 | } | |
316 | ||
317 | token = make_hs256_token(payload) | |
318 | expect do | |
319 | jwt_validator.verify(token) | |
320 | end.to raise_error(an_instance_of(OmniAuth::Auth0::TokenValidationError).and having_attributes({ | |
321 | message: "Issued At (iat) claim must be a number present in the ID token" | |
322 | })) | |
323 | end | |
324 | ||
325 | it 'should fail when authorize params has nonce but nonce is missing in the token' do | |
326 | payload = { | |
327 | iss: "https://#{domain}/", | |
328 | sub: 'sub', | |
329 | aud: client_id, | |
330 | exp: future_timecode, | |
331 | iat: past_timecode | |
332 | } | |
333 | ||
334 | token = make_hs256_token(payload) | |
335 | expect do | |
336 | jwt_validator.verify(token, { nonce: 'noncey' }) | |
337 | end.to raise_error(an_instance_of(OmniAuth::Auth0::TokenValidationError).and having_attributes({ | |
338 | message: "Nonce (nonce) claim must be a string present in the ID token" | |
339 | })) | |
340 | end | |
341 | ||
342 | it 'should fail when authorize params has nonce but token nonce does not match' do | |
343 | payload = { | |
344 | iss: "https://#{domain}/", | |
345 | sub: 'sub', | |
346 | aud: client_id, | |
347 | exp: future_timecode, | |
348 | iat: past_timecode, | |
349 | nonce: 'mismatch' | |
350 | } | |
351 | ||
352 | token = make_hs256_token(payload) | |
353 | expect do | |
354 | jwt_validator.verify(token, { nonce: 'noncey' }) | |
355 | end.to raise_error(an_instance_of(OmniAuth::Auth0::TokenValidationError).and having_attributes({ | |
356 | message: "Nonce (nonce) claim value mismatch in the ID token; expected (noncey), found (mismatch)" | |
357 | })) | |
358 | end | |
359 | ||
360 | it 'should fail when “aud” is an array of strings and azp claim is not present' do | |
361 | aud = [ | |
362 | client_id, | |
363 | "https://#{domain}/userinfo" | |
364 | ] | |
365 | payload = { | |
366 | iss: "https://#{domain}/", | |
367 | sub: 'sub', | |
368 | aud: aud, | |
369 | exp: future_timecode, | |
370 | iat: past_timecode | |
371 | } | |
372 | ||
373 | token = make_hs256_token(payload) | |
374 | expect do | |
375 | jwt_validator.verify(token) | |
376 | end.to raise_error(an_instance_of(OmniAuth::Auth0::TokenValidationError).and having_attributes({ | |
377 | message: "Authorized Party (azp) claim must be a string present in the ID token when Audience (aud) claim has multiple values" | |
378 | })) | |
379 | end | |
380 | ||
381 | it 'should fail when "azp" claim doesnt match the expected aud' do | |
382 | aud = [ | |
383 | client_id, | |
384 | "https://#{domain}/userinfo" | |
385 | ] | |
386 | payload = { | |
387 | iss: "https://#{domain}/", | |
388 | sub: 'sub', | |
389 | aud: aud, | |
390 | exp: future_timecode, | |
391 | iat: past_timecode, | |
392 | azp: 'not_expected' | |
393 | } | |
394 | ||
395 | token = make_hs256_token(payload) | |
396 | expect do | |
397 | jwt_validator.verify(token) | |
398 | end.to raise_error(an_instance_of(OmniAuth::Auth0::TokenValidationError).and having_attributes({ | |
399 | message: "Authorized Party (azp) claim mismatch in the ID token; expected (#{client_id}), found (not_expected)" | |
400 | })) | |
401 | end | |
402 | ||
403 | it 'should fail when “max_age” sent on the authentication request and this claim is not present' do | |
404 | payload = { | |
405 | iss: "https://#{domain}/", | |
406 | sub: 'sub', | |
407 | aud: client_id, | |
408 | exp: future_timecode, | |
409 | iat: past_timecode | |
410 | } | |
411 | ||
412 | token = make_hs256_token(payload) | |
413 | expect do | |
414 | jwt_validator.verify(token, { max_age: 60 }) | |
415 | end.to raise_error(an_instance_of(OmniAuth::Auth0::TokenValidationError).and having_attributes({ | |
416 | message: "Authentication Time (auth_time) claim must be a number present in the ID token when Max Age (max_age) is specified" | |
417 | })) | |
418 | end | |
419 | ||
420 | it 'should fail when “max_age” sent on the authentication request and this claim added the “max_age” value doesn’t represent a date in the future' do | |
421 | payload = { | |
422 | iss: "https://#{domain}/", | |
423 | sub: 'sub', | |
424 | aud: client_id, | |
425 | exp: future_timecode, | |
426 | iat: past_timecode, | |
427 | auth_time: past_timecode | |
428 | } | |
429 | ||
430 | token = make_hs256_token(payload) | |
431 | expect do | |
432 | jwt_validator.verify(token, { max_age: 60 }) | |
433 | end.to raise_error(an_instance_of(OmniAuth::Auth0::TokenValidationError).and having_attributes({ | |
434 | message: "Authentication Time (auth_time) claim in the ID token indicates that too much time has passed since the last end-user authentication. Current time (#{Time.now}) is after last auth time (#{Time.at(past_timecode + 60 + 60)})" | |
435 | })) | |
436 | end | |
437 | ||
438 | it 'should fail when “max_age” sent on the authentication request and this claim added the “max_age” value doesn’t represent a date in the future, outside the default leeway' do | |
439 | now = Time.now.to_i | |
440 | auth_time = now - 121 | |
441 | max_age = 60 | |
442 | payload = { | |
443 | iss: "https://#{domain}/", | |
444 | sub: 'sub', | |
445 | aud: client_id, | |
446 | exp: future_timecode, | |
447 | iat: past_timecode, | |
448 | auth_time: auth_time | |
449 | } | |
450 | ||
451 | token = make_hs256_token(payload) | |
452 | expect do | |
453 | jwt_validator.verify(token, { max_age: max_age }) | |
454 | # Time.at(auth_time + max_age + leeway | |
455 | end.to raise_error(an_instance_of(OmniAuth::Auth0::TokenValidationError).and having_attributes({ | |
456 | message: "Authentication Time (auth_time) claim in the ID token indicates that too much time has passed since the last end-user authentication. Current time (#{Time.now}) is after last auth time (#{Time.at(auth_time + max_age + 60)})" | |
457 | })) | |
458 | end | |
459 | ||
460 | it 'should verify when “max_age” sent on the authentication request and this claim added the “max_age” value doesn’t represent a date in the future, outside the default leeway' do | |
461 | now = Time.now.to_i | |
462 | auth_time = now - 119 | |
463 | max_age = 60 | |
464 | payload = { | |
465 | iss: "https://#{domain}/", | |
466 | sub: 'sub', | |
467 | aud: client_id, | |
468 | exp: future_timecode, | |
469 | iat: past_timecode, | |
470 | auth_time: auth_time | |
471 | } | |
472 | ||
473 | token = make_hs256_token(payload) | |
474 | id_token = jwt_validator.verify(token, { max_age: max_age }) | |
475 | expect(id_token['auth_time']).to eq(auth_time) | |
476 | end | |
477 | ||
478 | it 'should fail when authorize params has organization but org_id is missing in the token', focus: true do | |
479 | payload = { | |
480 | iss: "https://#{domain}/", | |
481 | sub: 'sub', | |
482 | aud: client_id, | |
483 | exp: future_timecode, | |
484 | iat: past_timecode | |
485 | } | |
486 | ||
487 | token = make_hs256_token(payload) | |
488 | expect do | |
489 | jwt_validator.verify(token, { organization: 'Test Org' }) | |
490 | end.to raise_error(an_instance_of(OmniAuth::Auth0::TokenValidationError).and having_attributes({ | |
491 | message: "Organization Id (org_id) claim must be a string present in the ID token" | |
492 | })) | |
493 | end | |
494 | ||
495 | it 'should fail when authorize params has organization but token org_id does not match', focus: true do | |
496 | payload = { | |
497 | iss: "https://#{domain}/", | |
498 | sub: 'sub', | |
499 | aud: client_id, | |
500 | exp: future_timecode, | |
501 | iat: past_timecode, | |
502 | org_id: 'Wrong Org' | |
503 | } | |
504 | ||
505 | token = make_hs256_token(payload) | |
506 | expect do | |
507 | jwt_validator.verify(token, { organization: 'Test Org' }) | |
508 | end.to raise_error(an_instance_of(OmniAuth::Auth0::TokenValidationError).and having_attributes({ | |
509 | message: "Organization Id (org_id) claim value mismatch in the ID token; expected 'Test Org', found 'Wrong Org'" | |
510 | })) | |
511 | end | |
512 | ||
513 | it 'should fail for RS256 token when kid is incorrect' do | |
514 | domain = 'example.org' | |
515 | sub = 'abc123' | |
516 | payload = { | |
517 | sub: sub, | |
518 | exp: future_timecode, | |
519 | iss: "https://#{domain}/", | |
520 | iat: past_timecode, | |
521 | aud: client_id | |
522 | } | |
523 | invalid_kid = 'invalid-kid' | |
524 | token = make_rs256_token(payload, invalid_kid) | |
525 | expect do | |
526 | verified_token = make_jwt_validator(opt_domain: domain).verify(token) | |
527 | end.to raise_error(an_instance_of(OmniAuth::Auth0::TokenValidationError).and having_attributes({ | |
528 | message: "Could not find a public key for Key ID (kid) 'invalid-kid'" | |
529 | })) | |
530 | end | |
531 | ||
532 | it 'should fail when RS256 token has invalid signature' do | |
533 | domain = 'example.org' | |
534 | sub = 'abc123' | |
535 | payload = { | |
536 | sub: sub, | |
537 | exp: future_timecode, | |
538 | iss: "https://#{domain}/", | |
539 | iat: past_timecode, | |
540 | aud: client_id | |
541 | } | |
542 | token = make_rs256_token(payload) + 'bad' | |
543 | expect do | |
544 | verified_token = make_jwt_validator(opt_domain: domain).verify(token) | |
545 | end.to raise_error(an_instance_of(JWT::VerificationError).and having_attributes({ | |
546 | message: "Signature verification raised" | |
547 | })) | |
548 | end | |
549 | ||
550 | it 'should fail when algorithm is not RS256 or HS256' do | |
551 | payload = { | |
552 | iss: "https://#{domain}/", | |
553 | sub: 'abc123', | |
554 | aud: client_id, | |
555 | exp: future_timecode, | |
556 | iat: past_timecode | |
557 | } | |
558 | token = JWT.encode payload, 'secret', 'HS384' | |
559 | expect do | |
560 | jwt_validator.verify(token) | |
561 | end.to raise_error(an_instance_of(OmniAuth::Auth0::TokenValidationError).and having_attributes({ | |
562 | message: "Signature algorithm of HS384 is not supported. Expected the ID token to be signed with RS256 or HS256" | |
563 | })) | |
564 | end | |
565 | ||
566 | it 'should fail when HS256 token has invalid signature' do | |
567 | payload = { | |
568 | iss: "https://#{domain}/", | |
569 | sub: 'abc123', | |
570 | aud: client_id, | |
571 | exp: future_timecode, | |
572 | iat: past_timecode | |
573 | } | |
574 | token = make_hs256_token(payload, 'bad_secret') | |
575 | expect do | |
576 | # validator is configured to use "CLIENT_SECRET" by default | |
577 | jwt_validator.verify(token) | |
578 | end.to raise_error(an_instance_of(JWT::VerificationError)) | |
579 | end | |
580 | ||
581 | it 'should verify a valid HS256 token with multiple audiences' do | |
582 | audience = [ | |
583 | client_id, | |
584 | "https://#{domain}/userinfo" | |
585 | ] | |
586 | payload = { | |
587 | iss: "https://#{domain}/", | |
588 | sub: 'sub', | |
589 | aud: audience, | |
590 | exp: future_timecode, | |
591 | iat: past_timecode, | |
592 | azp: client_id | |
593 | } | |
594 | token = make_hs256_token(payload) | |
595 | id_token = jwt_validator.verify(token) | |
596 | expect(id_token['aud']).to eq(audience) | |
597 | end | |
598 | ||
599 | it 'should verify a standard HS256 token' do | |
600 | sub = 'abc123' | |
601 | payload = { | |
602 | iss: "https://#{domain}/", | |
603 | sub: sub, | |
604 | aud: client_id, | |
605 | exp: future_timecode, | |
606 | iat: past_timecode | |
607 | } | |
608 | token = make_hs256_token(payload) | |
609 | verified_token = jwt_validator.verify(token) | |
610 | expect(verified_token['sub']).to eq(sub) | |
611 | end | |
612 | ||
613 | it 'should verify a standard RS256 token' do | |
614 | domain = 'example.org' | |
615 | sub = 'abc123' | |
616 | payload = { | |
617 | sub: sub, | |
618 | exp: future_timecode, | |
619 | iss: "https://#{domain}/", | |
620 | iat: past_timecode, | |
621 | aud: client_id | |
622 | } | |
623 | token = make_rs256_token(payload) | |
624 | verified_token = make_jwt_validator(opt_domain: domain).verify(token) | |
625 | expect(verified_token['sub']).to eq(sub) | |
626 | end | |
627 | ||
628 | it 'should verify a HS256 JWT signature when calling verify signature directly' do | |
629 | sub = 'abc123' | |
630 | payload = { | |
631 | iss: "https://#{domain}/", | |
632 | sub: sub, | |
633 | aud: client_id, | |
634 | exp: future_timecode, | |
635 | iat: past_timecode | |
636 | } | |
637 | token = make_hs256_token(payload) | |
638 | verified_token_signature = jwt_validator.verify_signature(token) | |
639 | expect(verified_token_signature[0]).to eq('CLIENT_SECRET') | |
640 | expect(verified_token_signature[1]).to eq('HS256') | |
641 | end | |
642 | ||
643 | it 'should verify a RS256 JWT signature verify signature directly' do | |
644 | domain = 'example.org' | |
645 | sub = 'abc123' | |
646 | payload = { | |
647 | sub: sub, | |
648 | exp: future_timecode, | |
649 | iss: "https://#{domain}/", | |
650 | iat: past_timecode, | |
651 | aud: client_id | |
652 | } | |
653 | token = make_rs256_token(payload) | |
654 | verified_token_signature = make_jwt_validator(opt_domain: domain).verify_signature(token) | |
655 | expect(verified_token_signature.length).to be(2) | |
656 | expect(verified_token_signature[0]).to be_a(OpenSSL::PKey::RSA) | |
657 | expect(verified_token_signature[1]).to eq('RS256') | |
658 | end | |
659 | end | |
660 | ||
661 | private | |
662 | ||
663 | def make_jwt_validator(opt_domain: domain, opt_issuer: nil) | |
664 | opts = OpenStruct.new( | |
665 | domain: opt_domain, | |
666 | client_id: client_id, | |
667 | client_secret: client_secret | |
668 | ) | |
669 | opts[:issuer] = opt_issuer unless opt_issuer.nil? | |
670 | ||
671 | OmniAuth::Auth0::JWTValidator.new(opts) | |
672 | end | |
673 | ||
674 | def make_hs256_token(payload = nil, secret = nil) | |
675 | payload = { sub: 'abc123' } if payload.nil? | |
676 | secret = client_secret if secret.nil? | |
677 | JWT.encode payload, secret, 'HS256' | |
678 | end | |
679 | ||
680 | def make_rs256_token(payload = nil, kid = nil) | |
681 | payload = { sub: 'abc123' } if payload.nil? | |
682 | kid = valid_jwks_kid if kid.nil? | |
683 | JWT.encode payload, rsa_private_key, 'RS256', kid: kid | |
684 | end | |
685 | ||
686 | def make_cert(private_key) | |
687 | cert = OpenSSL::X509::Certificate.new | |
688 | cert.issuer = OpenSSL::X509::Name.parse('/C=BE/O=Auth0/OU=Auth0/CN=Auth0') | |
689 | cert.subject = cert.issuer | |
690 | cert.not_before = Time.now | |
691 | cert.not_after = Time.now + 365 * 24 * 60 * 60 | |
692 | cert.public_key = private_key.public_key | |
693 | cert.serial = 0x0 | |
694 | cert.version = 2 | |
695 | ||
696 | ef = OpenSSL::X509::ExtensionFactory.new | |
697 | ef.subject_certificate = cert | |
698 | ef.issuer_certificate = cert | |
699 | cert.extensions = [ | |
700 | ef.create_extension('basicConstraints', 'CA:TRUE', true), | |
701 | ef.create_extension('subjectKeyIdentifier', 'hash') | |
702 | ] | |
703 | cert.add_extension ef.create_extension( | |
704 | 'authorityKeyIdentifier', | |
705 | 'keyid:always,issuer:always' | |
706 | ) | |
707 | ||
708 | cert.sign private_key, OpenSSL::Digest::SHA1.new | |
709 | end | |
710 | ||
711 | def stub_complete_jwks | |
712 | stub_request(:get, 'https://samples.auth0.com/.well-known/jwks.json') | |
713 | .to_return( | |
714 | headers: { 'Content-Type' => 'application/json' }, | |
715 | body: jwks.to_json, | |
716 | status: 200 | |
717 | ) | |
718 | end | |
719 | ||
720 | def stub_expected_jwks | |
721 | stub_request(:get, 'https://example.org/.well-known/jwks.json') | |
722 | .to_return( | |
723 | headers: { 'Content-Type' => 'application/json' }, | |
724 | body: valid_jwks, | |
725 | status: 200 | |
726 | ) | |
727 | end | |
728 | end |
0 | require 'spec_helper' | |
1 | require 'json' | |
2 | ||
3 | describe OmniAuth::Auth0::Telemetry do | |
4 | ||
5 | let(:test_class) { Class.new.extend(OmniAuth::Auth0::Telemetry) } | |
6 | ||
7 | describe 'telemetry' do | |
8 | ||
9 | it 'should have the correct SDK name' do | |
10 | expect(test_class.telemetry).to have_key(:name) | |
11 | expect(test_class.telemetry[:name]).to eq('omniauth-auth0') | |
12 | end | |
13 | ||
14 | it 'should have the correct SDK version' do | |
15 | expect(test_class.telemetry).to have_key(:version) | |
16 | expect(test_class.telemetry[:version]).to eq(OmniAuth::Auth0::VERSION) | |
17 | end | |
18 | ||
19 | it 'should include the Ruby version' do | |
20 | expect(test_class.telemetry).to have_key(:env) | |
21 | expect(test_class.telemetry[:env]).to have_key(:ruby) | |
22 | expect(test_class.telemetry[:env][:ruby]).to eq(RUBY_VERSION) | |
23 | end | |
24 | ||
25 | end | |
26 | ||
27 | end |
0 | # frozen_string_literal: true | |
1 | ||
0 | 2 | require 'spec_helper' |
3 | require 'jwt' | |
1 | 4 | |
2 | 5 | RSpec.shared_examples 'site has valid domain url' do |url| |
3 | 6 | it { expect(subject.site).to eq(url) } |
22 | 25 | end |
23 | 26 | |
24 | 27 | describe 'client_options' do |
25 | let(:subject) { auth0.client } | |
28 | let(:subject) { OmniAuth::Strategies::Auth0.new( | |
29 | application, | |
30 | client_id, | |
31 | client_secret, | |
32 | domain_url | |
33 | ).client } | |
26 | 34 | |
27 | 35 | context 'domain with https' do |
28 | 36 | let(:domain_url) { 'https://samples.auth0.com' } |
77 | 85 | expect(redirect_url).to have_query('state') |
78 | 86 | expect(redirect_url).to have_query('client_id') |
79 | 87 | expect(redirect_url).to have_query('redirect_uri') |
88 | expect(redirect_url).not_to have_query('auth0Client') | |
89 | expect(redirect_url).not_to have_query('connection') | |
90 | expect(redirect_url).not_to have_query('connection_scope') | |
91 | expect(redirect_url).not_to have_query('prompt') | |
92 | expect(redirect_url).not_to have_query('screen_hint') | |
93 | expect(redirect_url).not_to have_query('login_hint') | |
94 | expect(redirect_url).not_to have_query('organization') | |
95 | expect(redirect_url).not_to have_query('invitation') | |
96 | end | |
97 | ||
98 | it 'redirects to hosted login page' do | |
99 | get 'auth/auth0?connection=abcd' | |
100 | expect(last_response.status).to eq(302) | |
101 | redirect_url = last_response.headers['Location'] | |
102 | expect(redirect_url).to start_with('https://samples.auth0.com/authorize') | |
103 | expect(redirect_url).to have_query('response_type', 'code') | |
104 | expect(redirect_url).to have_query('state') | |
105 | expect(redirect_url).to have_query('client_id') | |
106 | expect(redirect_url).to have_query('redirect_uri') | |
107 | expect(redirect_url).to have_query('connection', 'abcd') | |
108 | expect(redirect_url).not_to have_query('auth0Client') | |
109 | expect(redirect_url).not_to have_query('connection_scope') | |
110 | expect(redirect_url).not_to have_query('prompt') | |
111 | expect(redirect_url).not_to have_query('screen_hint') | |
112 | expect(redirect_url).not_to have_query('login_hint') | |
113 | expect(redirect_url).not_to have_query('organization') | |
114 | expect(redirect_url).not_to have_query('invitation') | |
115 | end | |
116 | ||
117 | it 'redirects to the hosted login page with connection_scope' do | |
118 | get 'auth/auth0?connection_scope=identity_provider_scope' | |
119 | expect(last_response.status).to eq(302) | |
120 | redirect_url = last_response.headers['Location'] | |
121 | expect(redirect_url).to start_with('https://samples.auth0.com/authorize') | |
122 | expect(redirect_url) | |
123 | .to have_query('connection_scope', 'identity_provider_scope') | |
124 | end | |
125 | ||
126 | it 'redirects to hosted login page with prompt=login' do | |
127 | get 'auth/auth0?prompt=login' | |
128 | expect(last_response.status).to eq(302) | |
129 | redirect_url = last_response.headers['Location'] | |
130 | expect(redirect_url).to start_with('https://samples.auth0.com/authorize') | |
131 | expect(redirect_url).to have_query('response_type', 'code') | |
132 | expect(redirect_url).to have_query('state') | |
133 | expect(redirect_url).to have_query('client_id') | |
134 | expect(redirect_url).to have_query('redirect_uri') | |
135 | expect(redirect_url).to have_query('prompt', 'login') | |
136 | expect(redirect_url).not_to have_query('auth0Client') | |
137 | expect(redirect_url).not_to have_query('connection') | |
138 | expect(redirect_url).not_to have_query('login_hint') | |
139 | expect(redirect_url).not_to have_query('organization') | |
140 | expect(redirect_url).not_to have_query('invitation') | |
141 | end | |
142 | ||
143 | it 'redirects to hosted login page with screen_hint=signup' do | |
144 | get 'auth/auth0?screen_hint=signup' | |
145 | expect(last_response.status).to eq(302) | |
146 | redirect_url = last_response.headers['Location'] | |
147 | expect(redirect_url).to start_with('https://samples.auth0.com/authorize') | |
148 | expect(redirect_url).to have_query('response_type', 'code') | |
149 | expect(redirect_url).to have_query('state') | |
150 | expect(redirect_url).to have_query('client_id') | |
151 | expect(redirect_url).to have_query('redirect_uri') | |
152 | expect(redirect_url).to have_query('screen_hint', 'signup') | |
153 | expect(redirect_url).not_to have_query('auth0Client') | |
154 | expect(redirect_url).not_to have_query('connection') | |
155 | expect(redirect_url).not_to have_query('login_hint') | |
156 | expect(redirect_url).not_to have_query('organization') | |
157 | expect(redirect_url).not_to have_query('invitation') | |
158 | end | |
159 | ||
160 | it 'redirects to hosted login page with organization=TestOrg and invitation=TestInvite' do | |
161 | get 'auth/auth0?organization=TestOrg&invitation=TestInvite' | |
162 | expect(last_response.status).to eq(302) | |
163 | redirect_url = last_response.headers['Location'] | |
164 | expect(redirect_url).to start_with('https://samples.auth0.com/authorize') | |
165 | expect(redirect_url).to have_query('response_type', 'code') | |
166 | expect(redirect_url).to have_query('state') | |
167 | expect(redirect_url).to have_query('client_id') | |
168 | expect(redirect_url).to have_query('redirect_uri') | |
169 | expect(redirect_url).to have_query('organization', 'TestOrg') | |
170 | expect(redirect_url).to have_query('invitation', 'TestInvite') | |
171 | expect(redirect_url).not_to have_query('auth0Client') | |
172 | expect(redirect_url).not_to have_query('connection') | |
173 | expect(redirect_url).not_to have_query('connection_scope') | |
174 | expect(redirect_url).not_to have_query('prompt') | |
175 | expect(redirect_url).not_to have_query('screen_hint') | |
176 | expect(redirect_url).not_to have_query('login_hint') | |
177 | end | |
178 | ||
179 | it 'redirects to hosted login page with login_hint=example@mail.com' do | |
180 | get 'auth/auth0?login_hint=example@mail.com' | |
181 | expect(last_response.status).to eq(302) | |
182 | redirect_url = last_response.headers['Location'] | |
183 | expect(redirect_url).to start_with('https://samples.auth0.com/authorize') | |
184 | expect(redirect_url).to have_query('response_type', 'code') | |
185 | expect(redirect_url).to have_query('state') | |
186 | expect(redirect_url).to have_query('client_id') | |
187 | expect(redirect_url).to have_query('redirect_uri') | |
188 | expect(redirect_url).to have_query('login_hint', 'example@mail.com') | |
189 | expect(redirect_url).not_to have_query('auth0Client') | |
190 | expect(redirect_url).not_to have_query('connection') | |
191 | expect(redirect_url).not_to have_query('connection_scope') | |
192 | expect(redirect_url).not_to have_query('prompt') | |
193 | expect(redirect_url).not_to have_query('screen_hint') | |
194 | expect(redirect_url).not_to have_query('organization') | |
195 | expect(redirect_url).not_to have_query('invitation') | |
80 | 196 | end |
81 | 197 | |
82 | 198 | describe 'callback' do |
84 | 200 | let(:expires_in) { 2000 } |
85 | 201 | let(:token_type) { 'bearer' } |
86 | 202 | let(:refresh_token) { 'refresh token' } |
87 | let(:id_token) { 'id token' } | |
203 | let(:telemetry_value) { Class.new.extend(OmniAuth::Auth0::Telemetry).telemetry_encoded } | |
88 | 204 | |
89 | 205 | let(:user_id) { 'user identifier' } |
90 | 206 | let(:state) { SecureRandom.hex(8) } |
94 | 210 | let(:email) { 'mail@mail.com' } |
95 | 211 | let(:email_verified) { true } |
96 | 212 | |
213 | let(:id_token) do | |
214 | payload = {} | |
215 | payload['sub'] = user_id | |
216 | payload['iss'] = "#{domain_url}/" | |
217 | payload['aud'] = client_id | |
218 | payload['name'] = name | |
219 | payload['nickname'] = nickname | |
220 | payload['picture'] = picture | |
221 | payload['email'] = email | |
222 | payload['email_verified'] = email_verified | |
223 | ||
224 | JWT.encode payload, client_secret, 'HS256' | |
225 | end | |
226 | ||
97 | 227 | let(:oauth_response) do |
98 | 228 | { |
99 | 229 | access_token: access_token, |
111 | 241 | } |
112 | 242 | end |
113 | 243 | |
114 | let(:basic_user_info) { { sub: user_id } } | |
115 | let(:oidc_user_info) do | |
116 | { | |
117 | sub: user_id, | |
118 | name: name, | |
119 | nickname: nickname, | |
120 | email: email, | |
121 | picture: picture, | |
122 | email_verified: email_verified | |
123 | } | |
124 | end | |
244 | let(:basic_user_info) { { "sub" => user_id, "name" => name } } | |
125 | 245 | |
126 | 246 | def stub_auth(body) |
127 | 247 | stub_request(:post, 'https://samples.auth0.com/oauth/token') |
248 | .with(headers: { 'Auth0-Client' => telemetry_value }) | |
128 | 249 | .to_return( |
129 | 250 | headers: { 'Content-Type' => 'application/json' }, |
130 | 251 | body: MultiJson.encode(body) |
148 | 269 | WebMock.reset! |
149 | 270 | end |
150 | 271 | |
151 | let(:subject) { MultiJson.decode(last_response.body) } | |
272 | let(:subject) do | |
273 | MultiJson.decode(last_response.body) | |
274 | end | |
152 | 275 | |
153 | 276 | context 'basic oauth' do |
154 | 277 | before do |
167 | 290 | expect(subject['credentials']['expires_at']).to_not be_nil |
168 | 291 | end |
169 | 292 | |
170 | it 'has basic values' do | |
293 | it 'has basic values' do | |
171 | 294 | expect(subject['provider']).to eq('auth0') |
172 | 295 | expect(subject['uid']).to eq(user_id) |
173 | expect(subject['info']['name']).to eq(user_id) | |
296 | expect(subject['info']['name']).to eq(name) | |
297 | end | |
298 | ||
299 | it 'should use the user info endpoint' do | |
300 | expect(subject['extra']['raw_info']).to eq(basic_user_info) | |
174 | 301 | end |
175 | 302 | end |
176 | 303 | |
196 | 323 | context 'oidc' do |
197 | 324 | before do |
198 | 325 | stub_auth(oidc_response) |
199 | stub_userinfo(oidc_user_info) | |
200 | 326 | trigger_callback |
201 | 327 | end |
202 | 328 | |
272 | 398 | uri = redirect_uri(actual) |
273 | 399 | query = query(uri) |
274 | 400 | if value.nil? |
275 | query[key].length == 1 | |
401 | query.key?(key) | |
276 | 402 | else |
277 | 403 | query[key] == [value] |
278 | 404 | end |
0 | { | |
1 | "keys": [ | |
2 | { | |
3 | "alg": "RS256", | |
4 | "kty": "RSA", | |
5 | "use": "sig", | |
6 | "x5c": [ | |
7 | "MIIDCzCCAfOgAwIBAgIJAJP6qydiMpsuMA0GCSqGSIb3DQEBBQUAMBwxGjAYBgNVBAMMEXNhbXBsZXMuYXV0aDAuY29tMB4XDTE0MDUyNjIyMDA1MFoXDTI4MDIwMjIyMDA1MFowHDEaMBgGA1UEAwwRc2FtcGxlcy5hdXRoMC5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCkH4CFGSJ4s3mwCBzaGGwxa9Jxzfb1ia4nUumxbsuaB7PClZZgrNQiOR3MXVNV9W6F1D+wjT6oFHOo7TOkVI22I/ff3XZTE0F35UUHGWRtiQ4LdZxwOPTed2Lax3F2DEyl3Y0CguUKbq2sSghvHYcggM6aj3N53VBsnBh/kdrURDLx1RYqBIL6Fvkhb/V/v/u9UKhZM0CDQRef9FZ7R8q9ie9cnbDOj1dT9d64kiJIYtTraG0gOrs4LI+4KK0EZu5R7Uo053IK7kfNasWhDkl8yxNYkDxwfcIuAcDmLgLnAI4tfW5beJuw+/w75PO/EwzwsnvppXaAz7e3Wf8g1yWFAgMBAAGjUDBOMB0GA1UdDgQWBBTsmytFLNox+NUZdTNlCUL3hHrngTAfBgNVHSMEGDAWgBTsmytFLNox+NUZdTNlCUL3hHrngTAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4IBAQAodbRX/34LnWB70l8dpDF1neDoG29F0XdpE9ICWHeWB1gb/FvJ5UMy9/pnL0DI3mPwkTDDob+16Zc68o6dT6sH3vEUP1iRreJlFADEmJZjrH9P4Y7ttx3G2Uw2RU5uucXIqiyMDBrQo4vx4Lnghl+b/WYbZJgzLfZLgkOEjcznS0Yi5Wdz6MvaL3FehSfweHyrjmxz0e8elHq7VY8OqRA+4PmUBce9BgDCk9fZFjgj8l0m9Vc5pPKSY9LMmTyrYkeDr/KppqdXKOCHmv7AIGb6rMCtbkIL/CM7Bh9Hx78/UKAz87Sl9A1yXVNjKbZwOEW60ORIwJmd8Tv46gJF+/rV" | |
8 | ], | |
9 | "n": "pB-AhRkieLN5sAgc2hhsMWvScc329YmuJ1LpsW7LmgezwpWWYKzUIjkdzF1TVfVuhdQ_sI0-qBRzqO0zpFSNtiP33912UxNBd-VFBxlkbYkOC3WccDj03ndi2sdxdgxMpd2NAoLlCm6trEoIbx2HIIDOmo9zed1QbJwYf5Ha1EQy8dUWKgSC-hb5IW_1f7_7vVCoWTNAg0EXn_RWe0fKvYnvXJ2wzo9XU_XeuJIiSGLU62htIDq7OCyPuCitBGbuUe1KNOdyCu5HzWrFoQ5JfMsTWJA8cH3CLgHA5i4C5wCOLX1uW3ibsPv8O-TzvxMM8LJ76aV2gM-3t1n_INclhQ", | |
10 | "e": "AQAB", | |
11 | "kid": "NkJCQzIyQzRBMEU4NjhGNUU4MzU4RkY0M0ZDQzkwOUQ0Q0VGNUMwQg", | |
12 | "x5t": "NkJCQzIyQzRBMEU4NjhGNUU4MzU4RkY0M0ZDQzkwOUQ0Q0VGNUMwQg" | |
13 | }, | |
14 | { | |
15 | "alg": "RS256", | |
16 | "kty": "RSA", | |
17 | "use": "sig", | |
18 | "x5c": [ | |
19 | "MIIC8DCCAdigAwIBAgIJ4pL5sRgcIYGZMA0GCSqGSIb3DQEBBQUAMB8xHTAbBgNVBAMTFGxiYWxtYWNlZGEuYXV0aDAuY29tMB4XDTE1MTIxMjE5MDczM1oXDTI5MDgyMDE5MDczM1owHzEdMBsGA1UEAxMUbGJhbG1hY2VkYS5hdXRoMC5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDPoo5DA/X8suAZujdmD2D88Ggtu8G/kuLUdEuj1W3+wzmFcEqQpE532rg8L0uppWKAbmLWzkuwyioNDhWwCtXnug3BFQf5Lrc6nTxjk4ZQt/HdsYWCGSSZueMUG/3I+2PSql3atD2nedjY6Z9hWU8kzOjF9wzkLMgPf/OYpuz9A+6d+/K8jApRPfsQ1LDVWDG8YRtj+IyHhSvXS+cK03iuD7yVLKkIZuoS8ymMJpnZONHGds/3P9pHY29KqliSYW0eGEX3BIarZG06gRJ+88WUbRi9+rfVAoGLq++S+bc021txK+qYS3nknhY0uv/ODBb4eeycuDjjdyLBCShVvbXFAgMBAAGjLzAtMAwGA1UdEwQFMAMBAf8wHQYDVR0OBBYEFG38TTjyzhRmpK7MXfvBXDcBtYJ3MA0GCSqGSIb3DQEBBQUAA4IBAQCLNW+rA25tjHs6Sa9VPgBfMMLd1PIEgMpQhET9JqpGYUgB+0q1leXw1cwh14x/6PF2oo3jPOMW+wCDA7KAVKYewYSr/Enph+zNFPaq2YQL9dCsVFcBsnEGznwZaqHrqxQDX9S2Ek6E9jNsuBCSpAPcTsfbn2TXz77V+HZ/4tbwRvYEX1S5agiZFyjZzJMiZU1KQzP5PhfzD6RPl5KTK2PYRhVdXwyuFxOdJzCzOC9E/Uw30Zd6+9oHmoNfvJr8BRy67YWjXaQAh2m8e+zv/dEzPimgvaLmI1yz4W+93dJy3NdMuCvObOqA534tviv5PkV57ewXAnWPbxyBHr57HdQ1" | |
20 | ], | |
21 | "n": "z6KOQwP1_LLgGbo3Zg9g_PBoLbvBv5Li1HRLo9Vt_sM5hXBKkKROd9q4PC9LqaVigG5i1s5LsMoqDQ4VsArV57oNwRUH-S63Op08Y5OGULfx3bGFghkkmbnjFBv9yPtj0qpd2rQ9p3nY2OmfYVlPJMzoxfcM5CzID3_zmKbs_QPunfvyvIwKUT37ENSw1VgxvGEbY_iMh4Ur10vnCtN4rg-8lSypCGbqEvMpjCaZ2TjRxnbP9z_aR2NvSqpYkmFtHhhF9wSGq2RtOoESfvPFlG0Yvfq31QKBi6vvkvm3NNtbcSvqmEt55J4WNLr_zgwW-HnsnLg443ciwQkoVb21xQ", | |
22 | "e": "AQAB", | |
23 | "kid": "RUVBOTVEMEZBMTA5NDAzNEQzNTZGNzMyMTI4MzU1RkNFQzhCQTM0Mg", | |
24 | "x5t": "RUVBOTVEMEZBMTA5NDAzNEQzNTZGNzMyMTI4MzU1RkNFQzhCQTM0Mg" | |
25 | } | |
26 | ] | |
27 | }⏎ |
0 | $LOAD_PATH.unshift File.expand_path('..', __FILE__) | |
1 | $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) | |
0 | $LOAD_PATH.unshift File.expand_path(__dir__) | |
1 | $LOAD_PATH.unshift File.expand_path('../lib', __dir__) | |
2 | 2 | |
3 | 3 | require 'simplecov' |
4 | if ENV['COVERAGE'] | |
5 | SimpleCov.start do | |
6 | minimum_coverage(89.8) | |
7 | end | |
4 | SimpleCov.start | |
5 | ||
6 | if ENV['CI'] == 'true' | |
7 | require 'codecov' | |
8 | SimpleCov.formatter = SimpleCov::Formatter::Codecov | |
8 | 9 | end |
10 | ||
9 | 11 | require 'rspec' |
10 | 12 | require 'rack/test' |
11 | 13 | require 'webmock/rspec' |