rodauth-oauth 0.0.6 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +65 -5
- data/README.md +42 -20
- data/lib/generators/roda/oauth/templates/db/migrate/create_rodauth_oauth.rb +4 -1
- data/lib/rodauth/features/oauth.rb +116 -92
- data/lib/rodauth/features/oauth_http_mac.rb +2 -0
- data/lib/rodauth/features/oauth_jwt.rb +50 -23
- data/lib/rodauth/features/oidc.rb +267 -0
- data/lib/rodauth/oauth/version.rb +1 -1
- metadata +6 -5
    
        checksums.yaml
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            ---
         | 
| 2 2 | 
             
            SHA256:
         | 
| 3 | 
            -
              metadata.gz:  | 
| 4 | 
            -
              data.tar.gz:  | 
| 3 | 
            +
              metadata.gz: 3eac600d006a2c78509f608575db062b7ba6d67356b890c7d38414b9b82875f9
         | 
| 4 | 
            +
              data.tar.gz: c37fc18c093f546023481a88cc526c5a0b721b1a3bfeac827c21184e0583071b
         | 
| 5 5 | 
             
            SHA512:
         | 
| 6 | 
            -
              metadata.gz:  | 
| 7 | 
            -
              data.tar.gz:  | 
| 6 | 
            +
              metadata.gz: 0a04fdb5ab370ed5736208cbd4ccb1e6da801af52cd68004625a21008c4a10a04bc143d99c9e1a71bccb9fad882fc3cff27d9c0900689dbd5cf6c0616e4d43a0
         | 
| 7 | 
            +
              data.tar.gz: e6d5cb6e8ff31d64eb588fa39ad6a1e7bb1ae9416adac64e5f9a21bf451ffd4115a8c2d3bb8759f1a32192a88a4a401336e1baca7621a33934dc1b2a873c8402
         | 
    
        data/CHANGELOG.md
    CHANGED
    
    | @@ -2,8 +2,58 @@ | |
| 2 2 |  | 
| 3 3 | 
             
            ## master
         | 
| 4 4 |  | 
| 5 | 
            +
            ### 0.1.0
         | 
| 6 | 
            +
             | 
| 7 | 
            +
            (31/7/2020)
         | 
| 8 | 
            +
             | 
| 9 | 
            +
            #### Features
         | 
| 10 | 
            +
             | 
| 11 | 
            +
            ##### OpenID
         | 
| 12 | 
            +
             | 
| 13 | 
            +
            `rodauth-oauth` now ships with support for [OpenID Connect](https://openid.net/connect/). In order to enable, you have to:
         | 
| 14 | 
            +
             | 
| 15 | 
            +
            ```ruby
         | 
| 16 | 
            +
            plugin :rodauth do
         | 
| 17 | 
            +
              enable :oidc
         | 
| 18 | 
            +
            end
         | 
| 19 | 
            +
            ```
         | 
| 20 | 
            +
             | 
| 21 | 
            +
            For more info about integrating it, [check the wiki](https://gitlab.com/honeyryderchuck/rodauth-oauth/-/wikis/home#openid-connect-since-v01).
         | 
| 22 | 
            +
             | 
| 23 | 
            +
            It supports omniauth openID integrations out-of-the-box, [check the OpenID example, which integrates with omniauth_openid_connect](https://gitlab.com/honeyryderchuck/rodauth-oauth/-/tree/master/examples).
         | 
| 24 | 
            +
             | 
| 25 | 
            +
            #### Improvements
         | 
| 26 | 
            +
             | 
| 27 | 
            +
            * JWT: `sub` claim now also handles "pairwise" subjects. For that, you have to set the `oauth_jwt_subject_type` option (`"public"` or `"pairwise"`) and `oauth_jwt_subject_secret` (will be used for salting the `sub` when the type is `"pairwise"`).
         | 
| 28 | 
            +
            * JWT: `auth_time` claim is now supported; if your application uses the `rodauth` feature `:account_expiration`, it'll use the `last_account_login_at` method, otherwise you can set the `last_account_login_at` option:
         | 
| 29 | 
            +
             | 
| 30 | 
            +
            ```ruby
         | 
| 31 | 
            +
            last_account_login_at do
         | 
| 32 | 
            +
              convert_timestamp(db[accounts_table].where(account_id_column => account_id).get(:that_column_where_you_keep_the_data))
         | 
| 33 | 
            +
            end
         | 
| 34 | 
            +
            ```
         | 
| 35 | 
            +
            * JWT: `iss` claim now defaults to `authorization_server_url` when not defined;
         | 
| 36 | 
            +
            * JWT: `aud` claim now defaults to the token application's client ID (`client_id` claim was removed as a result);
         | 
| 37 | 
            +
             | 
| 38 | 
            +
             | 
| 39 | 
            +
             | 
| 40 | 
            +
            #### Breaking Changes
         | 
| 41 | 
            +
             | 
| 42 | 
            +
            `rodauth-oauth` URLs no longer have the `oauth-` prefix, so make sure you update your integrations accordingly, i.e. where you used to rely on `/oauth-authorize`, you'll have to use `/authorize`.
         | 
| 43 | 
            +
             | 
| 44 | 
            +
            URI schemes for client applications redirect URIs have to be `https`. In order to override this, set the `oauth_valid_uri_schemes` to an array of your expected URI schemes.
         | 
| 45 | 
            +
             | 
| 46 | 
            +
             | 
| 47 | 
            +
            #### Bugfixes
         | 
| 48 | 
            +
             | 
| 49 | 
            +
            * Authorization request submission can receive the `scope` as an array of values now, instead of only dealing with receiving a white-space separated list.
         | 
| 50 | 
            +
            * fixed trailing "/" in the "issuer" value in server metadata (`https://server.com/` -> `https://server.com`).
         | 
| 51 | 
            +
             | 
| 52 | 
            +
             | 
| 5 53 | 
             
            ### 0.0.6
         | 
| 6 54 |  | 
| 55 | 
            +
            (6/7/2020)
         | 
| 56 | 
            +
             | 
| 7 57 | 
             
            #### Features
         | 
| 8 58 |  | 
| 9 59 | 
             
            The `oauth_jwt` feature now supports JWT Secured Authorization Request (JAR) (see https://tools.ietf.org/html/draft-ietf-oauth-jwsreq-20). This means that client applications can send the authorization parameters inside a signed JWT. The client applications keeps the private key, while the authorization server **must** store a public key for the client application. For encrypted JWTs, the client application should use one of the public encryption keys exposed in the JWKs URI, to encrypt the JWT. Remember, **tokens must be signed then encrypted** (or just signed).
         | 
| @@ -25,7 +75,9 @@ The `oauth_jwt` feature now supports JWT Secured Authorization Request (JAR) (se | |
| 25 75 | 
             
            Removed React Javascript from example applications.
         | 
| 26 76 |  | 
| 27 77 |  | 
| 28 | 
            -
            ### 0.0.5 | 
| 78 | 
            +
            ### 0.0.5
         | 
| 79 | 
            +
             | 
| 80 | 
            +
            (26/6/2020)
         | 
| 29 81 |  | 
| 30 82 | 
             
            #### Features
         | 
| 31 83 |  | 
| @@ -62,7 +114,9 @@ It **requires** the authorization to implement the server metadata endpoint (`/. | |
| 62 114 | 
             
            * option `scopes_param` renamed to `scope_param`;
         | 
| 63 115 | 
             
            *
         | 
| 64 116 |  | 
| 65 | 
            -
            ## 0.0.4 | 
| 117 | 
            +
            ## 0.0.4
         | 
| 118 | 
            +
             | 
| 119 | 
            +
            (13/6/2020)
         | 
| 66 120 |  | 
| 67 121 | 
             
            ### Features
         | 
| 68 122 |  | 
| @@ -99,7 +153,9 @@ The `oauth_jwt` feature now allows the usage of access tokens to authorize the g | |
| 99 153 |  | 
| 100 154 | 
             
            * Fixed scope claim of JWT ("scopes" -> "scope");
         | 
| 101 155 |  | 
| 102 | 
            -
            ## 0.0.3 | 
| 156 | 
            +
            ## 0.0.3
         | 
| 157 | 
            +
             | 
| 158 | 
            +
            (5/6/2020)
         | 
| 103 159 |  | 
| 104 160 | 
             
            ### Features
         | 
| 105 161 |  | 
| @@ -131,7 +187,9 @@ end | |
| 131 187 | 
             
            * renamed the existing `use_oauth_implicit_grant_type` to `use_oauth_implicit_grant_type?`;
         | 
| 132 188 | 
             
            * It's now usable as JSON API (small caveat: POST authorize will still redirect on success...);
         | 
| 133 189 |  | 
| 134 | 
            -
            ## 0.0.2 | 
| 190 | 
            +
            ## 0.0.2
         | 
| 191 | 
            +
             | 
| 192 | 
            +
            (29/5/2020)
         | 
| 135 193 |  | 
| 136 194 | 
             
            ### Features
         | 
| 137 195 |  | 
| @@ -147,6 +205,8 @@ end | |
| 147 205 |  | 
| 148 206 | 
             
            * usage of client secret for authorizing the generation of tokens, as the spec mandates (and refraining from them when doing PKCE).
         | 
| 149 207 |  | 
| 150 | 
            -
            ## 0.0.1 | 
| 208 | 
            +
            ## 0.0.1
         | 
| 209 | 
            +
             | 
| 210 | 
            +
            (14/5/2020)
         | 
| 151 211 |  | 
| 152 212 | 
             
            Initial implementation of the Oauth 2.0 framework, with an example app done using roda.
         | 
    
        data/README.md
    CHANGED
    
    | @@ -1,13 +1,13 @@ | |
| 1 1 | 
             
            # Rodauth::Oauth
         | 
| 2 2 |  | 
| 3 | 
            -
            [](https://gitlab.com/honeyryderchuck/rodauth-oauth/-/ | 
| 4 | 
            -
            [](https://gitlab. | 
| 3 | 
            +
            [](https://gitlab.com/honeyryderchuck/rodauth-oauth/-/pipelines?page=1&ref=master)
         | 
| 4 | 
            +
            [](https://honeyryderchuck.gitlab.io/rodauth-oauth/coverage/#_AllFiles)
         | 
| 5 5 |  | 
| 6 6 | 
             
            This is an extension to the `rodauth` gem which implements the [OAuth 2.0 framework](https://tools.ietf.org/html/rfc6749) for an authorization server.
         | 
| 7 7 |  | 
| 8 8 | 
             
            ## Features
         | 
| 9 9 |  | 
| 10 | 
            -
            This gem implements:
         | 
| 10 | 
            +
            This gem implements the following RFCs and features of OAuth:
         | 
| 11 11 |  | 
| 12 12 | 
             
            * [The OAuth 2.0 protocol framework](https://tools.ietf.org/html/rfc6749):
         | 
| 13 13 | 
             
              * [Authorization grant flow](https://tools.ietf.org/html/rfc6749#section-1.3);
         | 
| @@ -24,6 +24,8 @@ This gem implements: | |
| 24 24 | 
             
            * [JWT Secured Authorization Requests](https://tools.ietf.org/html/draft-ietf-oauth-jwsreq-20);
         | 
| 25 25 | 
             
            * OAuth application and token management dashboards;
         | 
| 26 26 |  | 
| 27 | 
            +
            It also implements the [OpenID Connect layer](https://openid.net/connect/) on top of the OAuth features it provides.
         | 
| 28 | 
            +
             | 
| 27 29 | 
             
            This gem supports also rails (through [rodauth-rails]((https://github.com/janko/rodauth-rails))).
         | 
| 28 30 |  | 
| 29 31 |  | 
| @@ -43,6 +45,15 @@ Or install it yourself as: | |
| 43 45 |  | 
| 44 46 | 
             
                $ gem install rodauth-oauth
         | 
| 45 47 |  | 
| 48 | 
            +
             | 
| 49 | 
            +
            ## Resources
         | 
| 50 | 
            +
            |               |                                                             |
         | 
| 51 | 
            +
            | ------------- | ----------------------------------------------------------- |
         | 
| 52 | 
            +
            | Website       | https://honeyryderchuck.gitlab.io/rodauth-oauth/            |
         | 
| 53 | 
            +
            | Documentation | https://honeyryderchuck.gitlab.io/rodauth-oauth/rdoc/       |
         | 
| 54 | 
            +
            | Wiki          | https://gitlab.com/honeyryderchuck/rodauth-oauth/wikis/home |
         | 
| 55 | 
            +
            | CI            | https://gitlab.com/honeyryderchuck/rodauth-oauth/pipelines  |
         | 
| 56 | 
            +
             | 
| 46 57 | 
             
            ## Usage
         | 
| 47 58 |  | 
| 48 59 | 
             
            This tutorial assumes you already read the documentation and know how to set up `rodauth`. After that, integrating `roda-auth` will look like:
         | 
| @@ -86,7 +97,18 @@ route do |r| | |
| 86 97 | 
             
            end
         | 
| 87 98 | 
             
            ```
         | 
| 88 99 |  | 
| 89 | 
            -
             | 
| 100 | 
            +
             | 
| 101 | 
            +
            For OpenID, it's very similar to the example above:
         | 
| 102 | 
            +
             | 
| 103 | 
            +
            ```ruby
         | 
| 104 | 
            +
            plugin :rodauth do
         | 
| 105 | 
            +
              # enable it in the plugin
         | 
| 106 | 
            +
              enable :login, :openid
         | 
| 107 | 
            +
              oauth_application_default_scope %w[openid]
         | 
| 108 | 
            +
              oauth_application_scopes %w[openid email profile]
         | 
| 109 | 
            +
            end
         | 
| 110 | 
            +
            ```
         | 
| 111 | 
            +
             | 
| 90 112 |  | 
| 91 113 | 
             
            ### Example (TL;DR)
         | 
| 92 114 |  | 
| @@ -101,7 +123,7 @@ Generating tokens happens mostly server-to-server, so here's an example using: | |
| 101 123 |  | 
| 102 124 | 
             
            ```ruby
         | 
| 103 125 | 
             
            require "httpx"
         | 
| 104 | 
            -
            response = HTTPX.post("https://auth_server/ | 
| 126 | 
            +
            response = HTTPX.post("https://auth_server/token",json: {
         | 
| 105 127 | 
             
                              client_id: ENV["OAUTH_CLIENT_ID"],
         | 
| 106 128 | 
             
                              client_secret: ENV["OAUTH_CLIENT_SECRET"],
         | 
| 107 129 | 
             
                              grant_type: "authorization_code",
         | 
| @@ -115,7 +137,7 @@ puts payload #=> {"access_token" => "awr23f3h8f9d2h89...", "refresh_token" => "2 | |
| 115 137 | 
             
            ##### cURL
         | 
| 116 138 |  | 
| 117 139 | 
             
            ```
         | 
| 118 | 
            -
            > curl --data '{"client_id":"$OAUTH_CLIENT_ID","client_secret":"$OAUTH_CLIENT_SECRET","grant_type":"authorization_code","code":"oiweicnewdh32fhoi3hf3ihfo2ih3f2o3as"}' https://auth_server/ | 
| 140 | 
            +
            > curl --data '{"client_id":"$OAUTH_CLIENT_ID","client_secret":"$OAUTH_CLIENT_SECRET","grant_type":"authorization_code","code":"oiweicnewdh32fhoi3hf3ihfo2ih3f2o3as"}' https://auth_server/token
         | 
| 119 141 | 
             
            ```
         | 
| 120 142 |  | 
| 121 143 | 
             
            #### Refresh Token
         | 
| @@ -126,7 +148,7 @@ Refreshing expired tokens also happens mostly server-to-server, here's an exampl | |
| 126 148 |  | 
| 127 149 | 
             
            ```ruby
         | 
| 128 150 | 
             
            require "httpx"
         | 
| 129 | 
            -
            response = HTTPX.post("https://auth_server/ | 
| 151 | 
            +
            response = HTTPX.post("https://auth_server/token",json: {
         | 
| 130 152 | 
             
                              client_id: ENV["OAUTH_CLIENT_ID"],
         | 
| 131 153 | 
             
                              client_secret: ENV["OAUTH_CLIENT_SECRET"],
         | 
| 132 154 | 
             
                              grant_type: "refresh_token",
         | 
| @@ -140,7 +162,7 @@ puts payload #=> {"access_token" => "awr23f3h8f9d2h89...", "token_type" => "Bear | |
| 140 162 | 
             
            ##### cURL
         | 
| 141 163 |  | 
| 142 164 | 
             
            ```
         | 
| 143 | 
            -
            > curl -H "X-your-auth-scheme: $SERVER_KEY" --data '{"client_id":"$OAUTH_CLIENT_ID","client_secret":"$OAUTH_CLIENT_SECRET","grant_type":"token","token":"2r89hfef4j9f90d2j2390jf390g"}' https://auth_server/ | 
| 165 | 
            +
            > curl -H "X-your-auth-scheme: $SERVER_KEY" --data '{"client_id":"$OAUTH_CLIENT_ID","client_secret":"$OAUTH_CLIENT_SECRET","grant_type":"token","token":"2r89hfef4j9f90d2j2390jf390g"}' https://auth_server/token
         | 
| 144 166 | 
             
            ```
         | 
| 145 167 |  | 
| 146 168 | 
             
            #### Revoking tokens
         | 
| @@ -151,7 +173,7 @@ Token revocation can be done both by the idenntity owner or the application owne | |
| 151 173 | 
             
            require "httpx"
         | 
| 152 174 | 
             
            httpx = HTTPX.plugin(:basic_authorization)
         | 
| 153 175 | 
             
            response = httpx.basic_authentication(ENV["CLIENT_ID"], ENV["CLIENT_SECRET"])
         | 
| 154 | 
            -
                            .post("https://auth_server/ | 
| 176 | 
            +
                            .post("https://auth_server/revoke",json: {
         | 
| 155 177 | 
             
                              token_type_hint: "access_token", # can also be "refresh:tokn"
         | 
| 156 178 | 
             
                              token: "2r89hfef4j9f90d2j2390jf390g"
         | 
| 157 179 | 
             
                            })
         | 
| @@ -163,7 +185,7 @@ puts payload #=> {"access_token" => "awr23f3h8f9d2h89...", "token_type" => "Bear | |
| 163 185 | 
             
            ##### cURL
         | 
| 164 186 |  | 
| 165 187 | 
             
            ```
         | 
| 166 | 
            -
            > curl -H "X-your-auth-scheme: $SERVER_KEY" --data '{"client_id":"$OAUTH_CLIENT_ID","token_type_hint":"access_token","token":"2r89hfef4j9f90d2j2390jf390g"}' https://auth_server/ | 
| 188 | 
            +
            > curl -H "X-your-auth-scheme: $SERVER_KEY" --data '{"client_id":"$OAUTH_CLIENT_ID","token_type_hint":"access_token","token":"2r89hfef4j9f90d2j2390jf390g"}' https://auth_server/revoke
         | 
| 167 189 | 
             
            ```
         | 
| 168 190 |  | 
| 169 191 | 
             
            #### Token introspection
         | 
| @@ -174,7 +196,7 @@ Token revocation can be used to determine the state of a token (whether active, | |
| 174 196 | 
             
            require "httpx"
         | 
| 175 197 | 
             
            httpx = HTTPX.plugin(:basic_authorization)
         | 
| 176 198 | 
             
            response = httpx.basic_authentication(ENV["CLIENT_ID"], ENV["CLIENT_SECRET"])
         | 
| 177 | 
            -
                            .post("https://auth_server/ | 
| 199 | 
            +
                            .post("https://auth_server/introspect",json: {
         | 
| 178 200 | 
             
                              token_type_hint: "access_token", # can also be "refresh:tokn"
         | 
| 179 201 | 
             
                              token: "2r89hfef4j9f90d2j2390jf390g"
         | 
| 180 202 | 
             
                            })
         | 
| @@ -186,7 +208,7 @@ puts payload #=> {"active" => true, "scope" => "read write" .... | |
| 186 208 | 
             
            ##### cURL
         | 
| 187 209 |  | 
| 188 210 | 
             
            ```
         | 
| 189 | 
            -
            > curl -H "X-your-auth-scheme: $SERVER_KEY" --data '{"client_id":"$OAUTH_CLIENT_ID","token_type_hint":"access_token","token":"2r89hfef4j9f90d2j2390jf390g"}' https://auth_server/ | 
| 211 | 
            +
            > curl -H "X-your-auth-scheme: $SERVER_KEY" --data '{"client_id":"$OAUTH_CLIENT_ID","token_type_hint":"access_token","token":"2r89hfef4j9f90d2j2390jf390g"}' https://auth_server/revoke
         | 
| 190 212 | 
             
            ```
         | 
| 191 213 |  | 
| 192 214 | 
             
            ### Authorization Server Metadata
         | 
| @@ -243,10 +265,10 @@ The rodauth default setup expects the roda `render` plugin to be activated; by d | |
| 243 265 |  | 
| 244 266 | 
             
            Once you set it up, by default, the following endpoints will be available:
         | 
| 245 267 |  | 
| 246 | 
            -
            * `GET / | 
| 247 | 
            -
            * `POST / | 
| 248 | 
            -
            * `POST / | 
| 249 | 
            -
            * `POST / | 
| 268 | 
            +
            * `GET /authorize`: Loads the OAuth authorization HTML form;
         | 
| 269 | 
            +
            * `POST /authorize`: Responds to an OAuth authorization request, as [per the spec](https://tools.ietf.org/html/rfc6749#section-4);
         | 
| 270 | 
            +
            * `POST /token`: Generates OAuth tokens as [per the spec](https://tools.ietf.org/html/rfc6749#section-4.4.2);
         | 
| 271 | 
            +
            * `POST /revoke`: Revokes OAuth tokens as [per the spec](https://tools.ietf.org/html/rfc7009);
         | 
| 250 272 |  | 
| 251 273 | 
             
            ### OAuth applications
         | 
| 252 274 |  | 
| @@ -426,7 +448,7 @@ The "Proof Key for Code Exchange by OAuth Public Clients" (aka PKCE) flow, which | |
| 426 448 | 
             
            ```ruby
         | 
| 427 449 | 
             
            # with httpx
         | 
| 428 450 | 
             
            require "httpx"
         | 
| 429 | 
            -
            response = HTTPX.post("https://auth_server/ | 
| 451 | 
            +
            response = HTTPX.post("https://auth_server/token",json: {
         | 
| 430 452 | 
             
                              client_id: ENV["OAUTH_CLIENT_ID"],
         | 
| 431 453 | 
             
                              grant_type: "authorization_code",
         | 
| 432 454 | 
             
                              code: "oiweicnewdh32fhoi3hf3ihfo2ih3f2o3as",
         | 
| @@ -477,7 +499,7 @@ Generating an access token will deliver the following fields: | |
| 477 499 | 
             
            ```ruby
         | 
| 478 500 | 
             
            # with httpx
         | 
| 479 501 | 
             
            require "httpx"
         | 
| 480 | 
            -
            response = httpx.post("https://auth_server/ | 
| 502 | 
            +
            response = httpx.post("https://auth_server/token",json: {
         | 
| 481 503 | 
             
                              client_id: env["oauth_client_id"],
         | 
| 482 504 | 
             
                              client_secret: env["oauth_client_secret"],
         | 
| 483 505 | 
             
                              grant_type: "authorization_code",
         | 
| @@ -576,7 +598,7 @@ which adds an extra layer of protection. | |
| 576 598 |  | 
| 577 599 | 
             
            #### JWKS URI
         | 
| 578 600 |  | 
| 579 | 
            -
            A route is defined for getting the JWK Set in a JSON format; this is typically used by client applications, who need the JWK set to decode the JWT token. This URL is typically `https://oauth-server/ | 
| 601 | 
            +
            A route is defined for getting the JWK Set in a JSON format; this is typically used by client applications, who need the JWK set to decode the JWT token. This URL is typically `https://oauth-server/jwks`.
         | 
| 580 602 |  | 
| 581 603 | 
             
            #### JWT Bearer as authorization grant
         | 
| 582 604 |  | 
| @@ -585,7 +607,7 @@ One can emit a new access token by using the bearer access token as grant. This | |
| 585 607 | 
             
            ```ruby
         | 
| 586 608 | 
             
            # with httpx
         | 
| 587 609 | 
             
            require "httpx"
         | 
| 588 | 
            -
            response = httpx.post("https://auth_server/ | 
| 610 | 
            +
            response = httpx.post("https://auth_server/token",json: {
         | 
| 589 611 | 
             
                              grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
         | 
| 590 612 | 
             
                              assertion: "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOjEsImlzcyI6IkV4YW1wbGUiLCJpYXQiOjE1OTIwMDk1MDEsImNsaWVudF9pZCI6IkNMSUVOVF9JRCIsImV4cCI6MTU5MjAxMzEwMSwiYXVkIjpudWxsLCJzY29wZSI6InVzZXIucmVhZCB1c2VyLndyaXRlIiwianRpIjoiOGM1NTVjMjdiOWRjNDdmOTcyNWRkYzBhMjk0NzA1ZTA4NzFkY2JlN2Q5ZTNlMmVkNGE1ZTBiOGZlNTZlYzcxMSJ9.AlxKRtE3ec0mtyBSDx4VseND4eC6cH5ubtv8gfYxxsc"
         | 
| 591 613 | 
             
                            })
         | 
| @@ -29,7 +29,8 @@ class CreateRodauthOAuth < ActiveRecord::Migration<%= migration_version %> | |
| 29 29 | 
             
                  # uncomment to enable PKCE
         | 
| 30 30 | 
             
                  # t.string :code_challenge
         | 
| 31 31 | 
             
                  # t.string :code_challenge_method
         | 
| 32 | 
            -
             | 
| 32 | 
            +
                  # uncomment to use OIDC nonce
         | 
| 33 | 
            +
                  # t.string :nonce
         | 
| 33 34 | 
             
                  t.index(%i[oauth_application_id code], unique: true)
         | 
| 34 35 | 
             
                end
         | 
| 35 36 |  | 
| @@ -54,6 +55,8 @@ class CreateRodauthOAuth < ActiveRecord::Migration<%= migration_version %> | |
| 54 55 | 
             
                  t.datetime :revoked_at
         | 
| 55 56 | 
             
                  t.string :scopes, null: false
         | 
| 56 57 | 
             
                  t.datetime :created_at, null: false, default: -> { "CURRENT_TIMESTAMP" }
         | 
| 58 | 
            +
                  # uncomment to use OIDC nonce
         | 
| 59 | 
            +
                  # t.string :nonce
         | 
| 57 60 | 
             
                end
         | 
| 58 61 | 
             
              end
         | 
| 59 62 | 
             
            end
         | 
| @@ -43,7 +43,6 @@ module Rodauth | |
| 43 43 |  | 
| 44 44 | 
             
                before "authorize"
         | 
| 45 45 | 
             
                after "authorize"
         | 
| 46 | 
            -
                after "authorize_failure"
         | 
| 47 46 |  | 
| 48 47 | 
             
                before "token"
         | 
| 49 48 |  | 
| @@ -55,15 +54,13 @@ module Rodauth | |
| 55 54 | 
             
                before "create_oauth_application"
         | 
| 56 55 | 
             
                after "create_oauth_application"
         | 
| 57 56 |  | 
| 58 | 
            -
                error_flash "OAuth Authorization invalid parameters", "oauth_grant_valid_parameters"
         | 
| 59 | 
            -
             | 
| 60 57 | 
             
                error_flash "Please authorize to continue", "require_authorization"
         | 
| 61 58 | 
             
                error_flash "There was an error registering your oauth application", "create_oauth_application"
         | 
| 62 59 | 
             
                notice_flash "Your oauth application has been registered", "create_oauth_application"
         | 
| 63 60 |  | 
| 64 61 | 
             
                notice_flash "The oauth token has been revoked", "revoke_oauth_token"
         | 
| 65 62 |  | 
| 66 | 
            -
                view " | 
| 63 | 
            +
                view "authorize", "Authorize", "authorize"
         | 
| 67 64 | 
             
                view "oauth_applications", "Oauth Applications", "oauth_applications"
         | 
| 68 65 | 
             
                view "oauth_application", "Oauth Application", "oauth_application"
         | 
| 69 66 | 
             
                view "new_oauth_application", "New Oauth Application", "new_oauth_application"
         | 
| @@ -80,7 +77,7 @@ module Rodauth | |
| 80 77 | 
             
                auth_value_method :oauth_require_pkce, false
         | 
| 81 78 | 
             
                auth_value_method :oauth_pkce_challenge_method, "S256"
         | 
| 82 79 |  | 
| 83 | 
            -
                auth_value_method :oauth_valid_uri_schemes, %w[ | 
| 80 | 
            +
                auth_value_method :oauth_valid_uri_schemes, %w[https]
         | 
| 84 81 |  | 
| 85 82 | 
             
                auth_value_method :oauth_scope_separator, " "
         | 
| 86 83 |  | 
| @@ -148,9 +145,7 @@ module Rodauth | |
| 148 145 | 
             
                auth_value_method :oauth_application_scopes, SCOPES
         | 
| 149 146 | 
             
                auth_value_method :oauth_token_type, "bearer"
         | 
| 150 147 |  | 
| 151 | 
            -
                auth_value_method : | 
| 152 | 
            -
                auth_value_method :invalid_client, "Invalid client"
         | 
| 153 | 
            -
                auth_value_method :unauthorized_client, "Unauthorized client"
         | 
| 148 | 
            +
                auth_value_method :invalid_client_message, "Invalid client"
         | 
| 154 149 | 
             
                auth_value_method :invalid_grant_type_message, "Invalid grant type"
         | 
| 155 150 | 
             
                auth_value_method :invalid_grant_message, "Invalid grant"
         | 
| 156 151 | 
             
                auth_value_method :invalid_scope_message, "Invalid scope"
         | 
| @@ -195,11 +190,11 @@ module Rodauth | |
| 195 190 |  | 
| 196 191 | 
             
                def check_csrf?
         | 
| 197 192 | 
             
                  case request.path
         | 
| 198 | 
            -
                  when  | 
| 193 | 
            +
                  when token_path, introspect_path
         | 
| 199 194 | 
             
                    false
         | 
| 200 | 
            -
                  when  | 
| 195 | 
            +
                  when revoke_path
         | 
| 201 196 | 
             
                    !json_request?
         | 
| 202 | 
            -
                  when  | 
| 197 | 
            +
                  when authorize_path, %r{/#{oauth_applications_path}}
         | 
| 203 198 | 
             
                    only_json? ? false : super
         | 
| 204 199 | 
             
                  else
         | 
| 205 200 | 
             
                    super
         | 
| @@ -233,7 +228,15 @@ module Rodauth | |
| 233 228 | 
             
                end
         | 
| 234 229 |  | 
| 235 230 | 
             
                def scopes
         | 
| 236 | 
            -
                   | 
| 231 | 
            +
                  scope = request.params["scope"]
         | 
| 232 | 
            +
                  case scope
         | 
| 233 | 
            +
                  when Array
         | 
| 234 | 
            +
                    scope
         | 
| 235 | 
            +
                  when String
         | 
| 236 | 
            +
                    scope.split(" ")
         | 
| 237 | 
            +
                  when nil
         | 
| 238 | 
            +
                    [oauth_application_default_scope]
         | 
| 239 | 
            +
                  end
         | 
| 237 240 | 
             
                end
         | 
| 238 241 |  | 
| 239 242 | 
             
                def redirect_uri
         | 
| @@ -266,6 +269,8 @@ module Rodauth | |
| 266 269 |  | 
| 267 270 | 
             
                  return unless scheme.downcase == oauth_token_type
         | 
| 268 271 |  | 
| 272 | 
            +
                  return if token.empty?
         | 
| 273 | 
            +
             | 
| 269 274 | 
             
                  token
         | 
| 270 275 | 
             
                end
         | 
| 271 276 |  | 
| @@ -409,7 +414,7 @@ module Rodauth | |
| 409 414 | 
             
                  http = Net::HTTP.new(auth_url.host, auth_url.port)
         | 
| 410 415 | 
             
                  http.use_ssl = auth_url.scheme == "https"
         | 
| 411 416 |  | 
| 412 | 
            -
                  request = Net::HTTP::Post.new( | 
| 417 | 
            +
                  request = Net::HTTP::Post.new(introspect_path)
         | 
| 413 418 | 
             
                  request["content-type"] = json_response_content_type
         | 
| 414 419 | 
             
                  request["accept"] = json_response_content_type
         | 
| 415 420 | 
             
                  request.body = JSON.dump({ "token_type_hint" => token_type_hint, "token" => token })
         | 
| @@ -694,14 +699,14 @@ module Rodauth | |
| 694 699 | 
             
                  request.env["REQUEST_METHOD"] = "POST"
         | 
| 695 700 | 
             
                end
         | 
| 696 701 |  | 
| 697 | 
            -
                def create_oauth_grant
         | 
| 698 | 
            -
                  create_params | 
| 702 | 
            +
                def create_oauth_grant(create_params = {})
         | 
| 703 | 
            +
                  create_params.merge!(
         | 
| 699 704 | 
             
                    oauth_grants_account_id_column => account_id,
         | 
| 700 705 | 
             
                    oauth_grants_oauth_application_id_column => oauth_application[oauth_applications_id_column],
         | 
| 701 706 | 
             
                    oauth_grants_redirect_uri_column => redirect_uri,
         | 
| 702 707 | 
             
                    oauth_grants_expires_in_column => Time.now + oauth_grant_expires_in,
         | 
| 703 708 | 
             
                    oauth_grants_scopes_column => scopes.join(oauth_scope_separator)
         | 
| 704 | 
            -
                   | 
| 709 | 
            +
                  )
         | 
| 705 710 |  | 
| 706 711 | 
             
                  # Access Type flow
         | 
| 707 712 | 
             
                  if use_oauth_access_type?
         | 
| @@ -735,6 +740,45 @@ module Rodauth | |
| 735 740 | 
             
                  end
         | 
| 736 741 | 
             
                end
         | 
| 737 742 |  | 
| 743 | 
            +
                def do_authorize(redirect_url, query_params = [], fragment_params = [])
         | 
| 744 | 
            +
                  case param("response_type")
         | 
| 745 | 
            +
                  when "token"
         | 
| 746 | 
            +
                    redirect_response_error("invalid_request") unless use_oauth_implicit_grant_type?
         | 
| 747 | 
            +
             | 
| 748 | 
            +
                    fragment_params.replace(_do_authorize_token.map { |k, v| "#{k}=#{v}" })
         | 
| 749 | 
            +
                  when "code", "", nil
         | 
| 750 | 
            +
                    query_params.replace(_do_authorize_code.map { |k, v| "#{k}=#{v}" })
         | 
| 751 | 
            +
                  end
         | 
| 752 | 
            +
             | 
| 753 | 
            +
                  if param_or_nil("state")
         | 
| 754 | 
            +
                    if !fragment_params.empty?
         | 
| 755 | 
            +
                      fragment_params << "state=#{param('state')}"
         | 
| 756 | 
            +
                    else
         | 
| 757 | 
            +
                      query_params << "state=#{param('state')}"
         | 
| 758 | 
            +
                    end
         | 
| 759 | 
            +
                  end
         | 
| 760 | 
            +
             | 
| 761 | 
            +
                  query_params << redirect_url.query if redirect_url.query
         | 
| 762 | 
            +
             | 
| 763 | 
            +
                  redirect_url.query = query_params.join("&") unless query_params.empty?
         | 
| 764 | 
            +
                  redirect_url.fragment = fragment_params.join("&") unless fragment_params.empty?
         | 
| 765 | 
            +
                end
         | 
| 766 | 
            +
             | 
| 767 | 
            +
                def _do_authorize_code
         | 
| 768 | 
            +
                  { "code" => create_oauth_grant }
         | 
| 769 | 
            +
                end
         | 
| 770 | 
            +
             | 
| 771 | 
            +
                def _do_authorize_token
         | 
| 772 | 
            +
                  create_params = {
         | 
| 773 | 
            +
                    oauth_tokens_account_id_column => account_id,
         | 
| 774 | 
            +
                    oauth_tokens_oauth_application_id_column => oauth_application[oauth_applications_id_column],
         | 
| 775 | 
            +
                    oauth_tokens_scopes_column => scopes
         | 
| 776 | 
            +
                  }
         | 
| 777 | 
            +
                  oauth_token = generate_oauth_token(create_params, false)
         | 
| 778 | 
            +
             | 
| 779 | 
            +
                  json_access_token_payload(oauth_token)
         | 
| 780 | 
            +
                end
         | 
| 781 | 
            +
             | 
| 738 782 | 
             
                # Access Tokens
         | 
| 739 783 |  | 
| 740 784 | 
             
                def before_token
         | 
| @@ -760,27 +804,42 @@ module Rodauth | |
| 760 804 | 
             
                def create_oauth_token
         | 
| 761 805 | 
             
                  case param("grant_type")
         | 
| 762 806 | 
             
                  when "authorization_code"
         | 
| 763 | 
            -
                     | 
| 807 | 
            +
                    # fetch oauth grant
         | 
| 808 | 
            +
                    oauth_grant = db[oauth_grants_table].where(
         | 
| 809 | 
            +
                      oauth_grants_code_column => param("code"),
         | 
| 810 | 
            +
                      oauth_grants_redirect_uri_column => param("redirect_uri"),
         | 
| 811 | 
            +
                      oauth_grants_oauth_application_id_column => oauth_application[oauth_applications_id_column],
         | 
| 812 | 
            +
                      oauth_grants_revoked_at_column => nil
         | 
| 813 | 
            +
                    ).where(Sequel[oauth_grants_expires_in_column] >= Sequel::CURRENT_TIMESTAMP)
         | 
| 814 | 
            +
                                                        .for_update
         | 
| 815 | 
            +
                                                        .first
         | 
| 816 | 
            +
             | 
| 817 | 
            +
                    redirect_response_error("invalid_grant") unless oauth_grant
         | 
| 818 | 
            +
             | 
| 819 | 
            +
                    create_params = {
         | 
| 820 | 
            +
                      oauth_tokens_account_id_column => oauth_grant[oauth_grants_account_id_column],
         | 
| 821 | 
            +
                      oauth_tokens_oauth_application_id_column => oauth_grant[oauth_grants_oauth_application_id_column],
         | 
| 822 | 
            +
                      oauth_tokens_oauth_grant_id_column => oauth_grant[oauth_grants_id_column],
         | 
| 823 | 
            +
                      oauth_tokens_scopes_column => oauth_grant[oauth_grants_scopes_column]
         | 
| 824 | 
            +
                    }
         | 
| 825 | 
            +
                    create_oauth_token_from_authorization_code(oauth_grant, create_params)
         | 
| 764 826 | 
             
                  when "refresh_token"
         | 
| 765 | 
            -
                     | 
| 827 | 
            +
                    # fetch oauth token
         | 
| 828 | 
            +
                    oauth_token = oauth_token_by_refresh_token(param("refresh_token"))
         | 
| 829 | 
            +
             | 
| 830 | 
            +
                    redirect_response_error("invalid_grant") unless oauth_token
         | 
| 831 | 
            +
             | 
| 832 | 
            +
                    update_params = {
         | 
| 833 | 
            +
                      oauth_tokens_oauth_application_id_column => oauth_token[oauth_grants_oauth_application_id_column],
         | 
| 834 | 
            +
                      oauth_tokens_expires_in_column => Time.now + oauth_token_expires_in
         | 
| 835 | 
            +
                    }
         | 
| 836 | 
            +
                    create_oauth_token_from_token(oauth_token, update_params)
         | 
| 766 837 | 
             
                  else
         | 
| 767 838 | 
             
                    redirect_response_error("invalid_grant")
         | 
| 768 839 | 
             
                  end
         | 
| 769 840 | 
             
                end
         | 
| 770 841 |  | 
| 771 | 
            -
                def create_oauth_token_from_authorization_code( | 
| 772 | 
            -
                  # fetch oauth grant
         | 
| 773 | 
            -
                  oauth_grant = db[oauth_grants_table].where(
         | 
| 774 | 
            -
                    oauth_grants_code_column => param("code"),
         | 
| 775 | 
            -
                    oauth_grants_redirect_uri_column => param("redirect_uri"),
         | 
| 776 | 
            -
                    oauth_grants_oauth_application_id_column => oauth_application[oauth_applications_id_column],
         | 
| 777 | 
            -
                    oauth_grants_revoked_at_column => nil
         | 
| 778 | 
            -
                  ).where(Sequel[oauth_grants_expires_in_column] >= Sequel::CURRENT_TIMESTAMP)
         | 
| 779 | 
            -
                                                      .for_update
         | 
| 780 | 
            -
                                                      .first
         | 
| 781 | 
            -
             | 
| 782 | 
            -
                  redirect_response_error("invalid_grant") unless oauth_grant
         | 
| 783 | 
            -
             | 
| 842 | 
            +
                def create_oauth_token_from_authorization_code(oauth_grant, create_params)
         | 
| 784 843 | 
             
                  # PKCE
         | 
| 785 844 | 
             
                  if use_oauth_pkce?
         | 
| 786 845 | 
             
                    if oauth_grant[oauth_grants_code_challenge_column]
         | 
| @@ -792,13 +851,6 @@ module Rodauth | |
| 792 851 | 
             
                    end
         | 
| 793 852 | 
             
                  end
         | 
| 794 853 |  | 
| 795 | 
            -
                  create_params = {
         | 
| 796 | 
            -
                    oauth_tokens_account_id_column => oauth_grant[oauth_grants_account_id_column],
         | 
| 797 | 
            -
                    oauth_tokens_oauth_application_id_column => oauth_grant[oauth_grants_oauth_application_id_column],
         | 
| 798 | 
            -
                    oauth_tokens_oauth_grant_id_column => oauth_grant[oauth_grants_id_column],
         | 
| 799 | 
            -
                    oauth_tokens_scopes_column => oauth_grant[oauth_grants_scopes_column]
         | 
| 800 | 
            -
                  }
         | 
| 801 | 
            -
             | 
| 802 854 | 
             
                  # revoke oauth grant
         | 
| 803 855 | 
             
                  db[oauth_grants_table].where(oauth_grants_id_column => oauth_grant[oauth_grants_id_column])
         | 
| 804 856 | 
             
                                        .update(oauth_grants_revoked_at_column => Sequel::CURRENT_TIMESTAMP)
         | 
| @@ -809,19 +861,11 @@ module Rodauth | |
| 809 861 | 
             
                  generate_oauth_token(create_params, should_generate_refresh_token)
         | 
| 810 862 | 
             
                end
         | 
| 811 863 |  | 
| 812 | 
            -
                def create_oauth_token_from_token( | 
| 813 | 
            -
                   | 
| 814 | 
            -
                  oauth_token = oauth_token_by_refresh_token(param("refresh_token"))
         | 
| 815 | 
            -
             | 
| 816 | 
            -
                  redirect_response_error("invalid_grant") unless oauth_token && token_from_application?(oauth_token, oauth_application)
         | 
| 864 | 
            +
                def create_oauth_token_from_token(oauth_token, update_params)
         | 
| 865 | 
            +
                  redirect_response_error("invalid_grant") unless token_from_application?(oauth_token, oauth_application)
         | 
| 817 866 |  | 
| 818 867 | 
             
                  token = oauth_unique_id_generator
         | 
| 819 868 |  | 
| 820 | 
            -
                  update_params = {
         | 
| 821 | 
            -
                    oauth_tokens_oauth_application_id_column => oauth_token[oauth_grants_oauth_application_id_column],
         | 
| 822 | 
            -
                    oauth_tokens_expires_in_column => Time.now + oauth_token_expires_in
         | 
| 823 | 
            -
                  }
         | 
| 824 | 
            -
             | 
| 825 869 | 
             
                  if oauth_tokens_token_hash_column
         | 
| 826 870 | 
             
                    update_params[oauth_tokens_token_hash_column] = generate_token_hash(token)
         | 
| 827 871 | 
             
                  else
         | 
| @@ -995,7 +1039,7 @@ module Rodauth | |
| 995 1039 | 
             
                    throw_json_response_error(authorization_required_error_status, "invalid_client")
         | 
| 996 1040 | 
             
                  else
         | 
| 997 1041 | 
             
                    set_redirect_error_flash(require_authorization_error_flash)
         | 
| 998 | 
            -
                    redirect( | 
| 1042 | 
            +
                    redirect(authorize_path)
         | 
| 999 1043 | 
             
                  end
         | 
| 1000 1044 | 
             
                end
         | 
| 1001 1045 |  | 
| @@ -1075,7 +1119,7 @@ module Rodauth | |
| 1075 1119 |  | 
| 1076 1120 | 
             
                def oauth_server_metadata_body(path)
         | 
| 1077 1121 | 
             
                  issuer = base_url
         | 
| 1078 | 
            -
                  issuer += "/#{path}" if  | 
| 1122 | 
            +
                  issuer += "/#{path}" if path
         | 
| 1079 1123 |  | 
| 1080 1124 | 
             
                  responses_supported = %w[code]
         | 
| 1081 1125 | 
             
                  response_modes_supported = %w[query]
         | 
| @@ -1088,8 +1132,8 @@ module Rodauth | |
| 1088 1132 | 
             
                  end
         | 
| 1089 1133 | 
             
                  {
         | 
| 1090 1134 | 
             
                    issuer: issuer,
         | 
| 1091 | 
            -
                    authorization_endpoint:  | 
| 1092 | 
            -
                    token_endpoint:  | 
| 1135 | 
            +
                    authorization_endpoint: authorize_url,
         | 
| 1136 | 
            +
                    token_endpoint: token_url,
         | 
| 1093 1137 | 
             
                    registration_endpoint: "#{base_url}/#{oauth_applications_path}",
         | 
| 1094 1138 | 
             
                    scopes_supported: oauth_application_scopes,
         | 
| 1095 1139 | 
             
                    response_types_supported: responses_supported,
         | 
| @@ -1100,16 +1144,18 @@ module Rodauth | |
| 1100 1144 | 
             
                    ui_locales_supported: oauth_metadata_ui_locales_supported,
         | 
| 1101 1145 | 
             
                    op_policy_uri: oauth_metadata_op_policy_uri,
         | 
| 1102 1146 | 
             
                    op_tos_uri: oauth_metadata_op_tos_uri,
         | 
| 1103 | 
            -
                    revocation_endpoint:  | 
| 1147 | 
            +
                    revocation_endpoint: revoke_url,
         | 
| 1104 1148 | 
             
                    revocation_endpoint_auth_methods_supported: nil, # because it's client_secret_basic
         | 
| 1105 | 
            -
                    introspection_endpoint:  | 
| 1149 | 
            +
                    introspection_endpoint: introspect_url,
         | 
| 1106 1150 | 
             
                    introspection_endpoint_auth_methods_supported: %w[client_secret_basic],
         | 
| 1107 1151 | 
             
                    code_challenge_methods_supported: (use_oauth_pkce? ? oauth_pkce_challenge_method : nil)
         | 
| 1108 1152 | 
             
                  }
         | 
| 1109 1153 | 
             
                end
         | 
| 1110 1154 |  | 
| 1111 | 
            -
                # / | 
| 1112 | 
            -
                route(: | 
| 1155 | 
            +
                # /token
         | 
| 1156 | 
            +
                route(:token) do |r|
         | 
| 1157 | 
            +
                  next unless is_authorization_server?
         | 
| 1158 | 
            +
             | 
| 1113 1159 | 
             
                  before_token
         | 
| 1114 1160 |  | 
| 1115 1161 | 
             
                  r.post do
         | 
| @@ -1128,8 +1174,10 @@ module Rodauth | |
| 1128 1174 | 
             
                  end
         | 
| 1129 1175 | 
             
                end
         | 
| 1130 1176 |  | 
| 1131 | 
            -
                # / | 
| 1132 | 
            -
                route(: | 
| 1177 | 
            +
                # /introspect
         | 
| 1178 | 
            +
                route(:introspect) do |r|
         | 
| 1179 | 
            +
                  next unless is_authorization_server?
         | 
| 1180 | 
            +
             | 
| 1133 1181 | 
             
                  before_introspect
         | 
| 1134 1182 |  | 
| 1135 1183 | 
             
                  r.post do
         | 
| @@ -1159,8 +1207,10 @@ module Rodauth | |
| 1159 1207 | 
             
                  end
         | 
| 1160 1208 | 
             
                end
         | 
| 1161 1209 |  | 
| 1162 | 
            -
                # / | 
| 1163 | 
            -
                route(: | 
| 1210 | 
            +
                # /revoke
         | 
| 1211 | 
            +
                route(:revoke) do |r|
         | 
| 1212 | 
            +
                  next unless is_authorization_server?
         | 
| 1213 | 
            +
             | 
| 1164 1214 | 
             
                  before_revoke
         | 
| 1165 1215 |  | 
| 1166 1216 | 
             
                  r.post do
         | 
| @@ -1188,8 +1238,10 @@ module Rodauth | |
| 1188 1238 | 
             
                  end
         | 
| 1189 1239 | 
             
                end
         | 
| 1190 1240 |  | 
| 1191 | 
            -
                # / | 
| 1192 | 
            -
                route(: | 
| 1241 | 
            +
                # /authorize
         | 
| 1242 | 
            +
                route(:authorize) do |r|
         | 
| 1243 | 
            +
                  next unless is_authorization_server?
         | 
| 1244 | 
            +
             | 
| 1193 1245 | 
             
                  require_account
         | 
| 1194 1246 | 
             
                  validate_oauth_grant_params
         | 
| 1195 1247 | 
             
                  try_approval_prompt if use_oauth_access_type? && request.get?
         | 
| @@ -1201,39 +1253,11 @@ module Rodauth | |
| 1201 1253 | 
             
                  end
         | 
| 1202 1254 |  | 
| 1203 1255 | 
             
                  r.post do
         | 
| 1204 | 
            -
                     | 
| 1205 | 
            -
                    query_params = []
         | 
| 1206 | 
            -
                    fragment_params = []
         | 
| 1256 | 
            +
                    redirect_url = URI.parse(redirect_uri)
         | 
| 1207 1257 |  | 
| 1208 1258 | 
             
                    transaction do
         | 
| 1209 | 
            -
                       | 
| 1210 | 
            -
                      when "token"
         | 
| 1211 | 
            -
                        redirect_response_error("invalid_request") unless use_oauth_implicit_grant_type?
         | 
| 1212 | 
            -
             | 
| 1213 | 
            -
                        create_params = {
         | 
| 1214 | 
            -
                          oauth_tokens_account_id_column => account_id,
         | 
| 1215 | 
            -
                          oauth_tokens_oauth_application_id_column => oauth_application[oauth_applications_id_column],
         | 
| 1216 | 
            -
                          oauth_tokens_scopes_column => scopes
         | 
| 1217 | 
            -
                        }
         | 
| 1218 | 
            -
                        oauth_token = generate_oauth_token(create_params, false)
         | 
| 1219 | 
            -
             | 
| 1220 | 
            -
                        token_payload = json_access_token_payload(oauth_token)
         | 
| 1221 | 
            -
                        fragment_params.replace(token_payload.map { |k, v| "#{k}=#{v}" })
         | 
| 1222 | 
            -
                      when "code", "", nil
         | 
| 1223 | 
            -
                        code = create_oauth_grant
         | 
| 1224 | 
            -
                        query_params << "code=#{code}"
         | 
| 1225 | 
            -
                      else
         | 
| 1226 | 
            -
                        redirect_response_error("invalid_request")
         | 
| 1227 | 
            -
                      end
         | 
| 1228 | 
            -
                      after_authorize
         | 
| 1259 | 
            +
                      do_authorize(redirect_url)
         | 
| 1229 1260 | 
             
                    end
         | 
| 1230 | 
            -
             | 
| 1231 | 
            -
                    redirect_url = URI.parse(redirect_uri)
         | 
| 1232 | 
            -
                    query_params << "state=#{param('state')}" if param_or_nil("state")
         | 
| 1233 | 
            -
                    query_params << redirect_url.query if redirect_url.query
         | 
| 1234 | 
            -
                    redirect_url.query = query_params.join("&") unless query_params.empty?
         | 
| 1235 | 
            -
                    redirect_url.fragment = fragment_params.join("&") unless fragment_params.empty?
         | 
| 1236 | 
            -
             | 
| 1237 1261 | 
             
                    redirect(redirect_url.to_s)
         | 
| 1238 1262 | 
             
                  end
         | 
| 1239 1263 | 
             
                end
         | 
| @@ -2,6 +2,7 @@ | |
| 2 2 |  | 
| 3 3 | 
             
            module Rodauth
         | 
| 4 4 | 
             
              Feature.define(:oauth_http_mac) do
         | 
| 5 | 
            +
                # :nocov:
         | 
| 5 6 | 
             
                unless String.method_defined?(:delete_prefix)
         | 
| 6 7 | 
             
                  module PrefixExtensions
         | 
| 7 8 | 
             
                    refine(String) do
         | 
| @@ -27,6 +28,7 @@ module Rodauth | |
| 27 28 | 
             
                  end
         | 
| 28 29 | 
             
                  using(PrefixExtensions)
         | 
| 29 30 | 
             
                end
         | 
| 31 | 
            +
                # :nocov:
         | 
| 30 32 |  | 
| 31 33 | 
             
                depends :oauth
         | 
| 32 34 |  | 
| @@ -6,7 +6,10 @@ module Rodauth | |
| 6 6 | 
             
              Feature.define(:oauth_jwt) do
         | 
| 7 7 | 
             
                depends :oauth
         | 
| 8 8 |  | 
| 9 | 
            -
                auth_value_method : | 
| 9 | 
            +
                auth_value_method :oauth_jwt_subject_type, "public" # public, pairwise
         | 
| 10 | 
            +
                auth_value_method :oauth_jwt_subject_secret, nil # salt for pairwise generation
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                auth_value_method :oauth_jwt_token_issuer, nil
         | 
| 10 13 |  | 
| 11 14 | 
             
                auth_value_method :oauth_application_jws_jwk_column, nil
         | 
| 12 15 |  | 
| @@ -28,7 +31,8 @@ module Rodauth | |
| 28 31 | 
             
                auth_value_methods(
         | 
| 29 32 | 
             
                  :jwt_encode,
         | 
| 30 33 | 
             
                  :jwt_decode,
         | 
| 31 | 
            -
                  :jwks_set
         | 
| 34 | 
            +
                  :jwks_set,
         | 
| 35 | 
            +
                  :last_account_login_at
         | 
| 32 36 | 
             
                )
         | 
| 33 37 |  | 
| 34 38 | 
             
                JWKS = OAuth::TtlStore.new
         | 
| @@ -45,6 +49,12 @@ module Rodauth | |
| 45 49 |  | 
| 46 50 | 
             
                private
         | 
| 47 51 |  | 
| 52 | 
            +
                unless method_defined?(:last_account_login_at)
         | 
| 53 | 
            +
                  def last_account_login_at
         | 
| 54 | 
            +
                    nil
         | 
| 55 | 
            +
                  end
         | 
| 56 | 
            +
                end
         | 
| 57 | 
            +
             | 
| 48 58 | 
             
                def authorization_token
         | 
| 49 59 | 
             
                  return @authorization_token if defined?(@authorization_token)
         | 
| 50 60 |  | 
| @@ -57,8 +67,8 @@ module Rodauth | |
| 57 67 |  | 
| 58 68 | 
             
                    return unless jwt_token
         | 
| 59 69 |  | 
| 60 | 
            -
                    return if jwt_token["iss"] != oauth_jwt_token_issuer ||
         | 
| 61 | 
            -
                              jwt_token["aud"] != oauth_jwt_audience ||
         | 
| 70 | 
            +
                    return if jwt_token["iss"] != (oauth_jwt_token_issuer || authorization_server_url) ||
         | 
| 71 | 
            +
                              (oauth_jwt_audience && jwt_token["aud"] != oauth_jwt_audience) ||
         | 
| 62 72 | 
             
                              !jwt_token["sub"]
         | 
| 63 73 |  | 
| 64 74 | 
             
                    jwt_token
         | 
| @@ -169,11 +179,23 @@ module Rodauth | |
| 169 179 |  | 
| 170 180 | 
             
                  oauth_token = _generate_oauth_token(create_params)
         | 
| 171 181 |  | 
| 182 | 
            +
                  claims = jwt_claims(oauth_token)
         | 
| 183 | 
            +
             | 
| 184 | 
            +
                  # one of the points of using jwt is avoiding database lookups, so we put here all relevant
         | 
| 185 | 
            +
                  # token data.
         | 
| 186 | 
            +
                  claims[:scope] = oauth_token[oauth_tokens_scopes_column]
         | 
| 187 | 
            +
             | 
| 188 | 
            +
                  token = jwt_encode(claims)
         | 
| 189 | 
            +
             | 
| 190 | 
            +
                  oauth_token[oauth_tokens_token_column] = token
         | 
| 191 | 
            +
                  oauth_token
         | 
| 192 | 
            +
                end
         | 
| 193 | 
            +
             | 
| 194 | 
            +
                def jwt_claims(oauth_token)
         | 
| 172 195 | 
             
                  issued_at = Time.now.utc.to_i
         | 
| 173 196 |  | 
| 174 | 
            -
                   | 
| 175 | 
            -
                     | 
| 176 | 
            -
                    iss: oauth_jwt_token_issuer, # issuer
         | 
| 197 | 
            +
                  claims = {
         | 
| 198 | 
            +
                    iss: (oauth_jwt_token_issuer || authorization_server_url), # issuer
         | 
| 177 199 | 
             
                    iat: issued_at, # issued at
         | 
| 178 200 | 
             
                    #
         | 
| 179 201 | 
             
                    # sub  REQUIRED - as defined in section 4.1.2 of [RFC7519].  In case of
         | 
| @@ -184,20 +206,29 @@ module Rodauth | |
| 184 206 | 
             
                    # owner is involved, such as the client credentials grant, the value
         | 
| 185 207 | 
             
                    # of "sub" SHOULD correspond to an identifier the authorization
         | 
| 186 208 | 
             
                    # server uses to indicate the client application.
         | 
| 209 | 
            +
                    sub: jwt_subject(oauth_token),
         | 
| 187 210 | 
             
                    client_id: oauth_application[oauth_applications_client_id_column],
         | 
| 188 211 |  | 
| 189 212 | 
             
                    exp: issued_at + oauth_token_expires_in,
         | 
| 190 | 
            -
                    aud: oauth_jwt_audience | 
| 191 | 
            -
             | 
| 192 | 
            -
                    # one of the points of using jwt is avoiding database lookups, so we put here all relevant
         | 
| 193 | 
            -
                    # token data.
         | 
| 194 | 
            -
                    scope: oauth_token[oauth_tokens_scopes_column]
         | 
| 213 | 
            +
                    aud: (oauth_jwt_audience || oauth_application[oauth_applications_client_id_column])
         | 
| 195 214 | 
             
                  }
         | 
| 196 215 |  | 
| 197 | 
            -
                   | 
| 216 | 
            +
                  claims[:auth_time] = last_account_login_at.utc.to_i if last_account_login_at
         | 
| 198 217 |  | 
| 199 | 
            -
                   | 
| 200 | 
            -
             | 
| 218 | 
            +
                  claims
         | 
| 219 | 
            +
                end
         | 
| 220 | 
            +
             | 
| 221 | 
            +
                def jwt_subject(oauth_token)
         | 
| 222 | 
            +
                  case oauth_jwt_subject_type
         | 
| 223 | 
            +
                  when "public"
         | 
| 224 | 
            +
                    oauth_token[oauth_tokens_account_id_column]
         | 
| 225 | 
            +
                  when "pairwise"
         | 
| 226 | 
            +
                    id = oauth_token[oauth_tokens_account_id_column]
         | 
| 227 | 
            +
                    application_id = oauth_token[oauth_tokens_oauth_application_id_column]
         | 
| 228 | 
            +
                    Digest::SHA256.hexdigest("#{id}#{application_id}#{oauth_jwt_subject_secret}")
         | 
| 229 | 
            +
                  else
         | 
| 230 | 
            +
                    raise StandardError, "unexpected subject (#{oauth_jwt_subject_type})"
         | 
| 231 | 
            +
                  end
         | 
| 201 232 | 
             
                end
         | 
| 202 233 |  | 
| 203 234 | 
             
                def oauth_token_by_token(token, *)
         | 
| @@ -228,17 +259,11 @@ module Rodauth | |
| 228 259 | 
             
                def oauth_server_metadata_body(path)
         | 
| 229 260 | 
             
                  metadata = super
         | 
| 230 261 | 
             
                  metadata.merge! \
         | 
| 231 | 
            -
                    jwks_uri:  | 
| 262 | 
            +
                    jwks_uri: jwks_url,
         | 
| 232 263 | 
             
                    token_endpoint_auth_signing_alg_values_supported: [oauth_jwt_algorithm]
         | 
| 233 264 | 
             
                  metadata
         | 
| 234 265 | 
             
                end
         | 
| 235 266 |  | 
| 236 | 
            -
                def token_from_application?(oauth_token, oauth_application)
         | 
| 237 | 
            -
                  return super unless oauth_token["sub"] # naive check on whether it's a jwt token
         | 
| 238 | 
            -
             | 
| 239 | 
            -
                  oauth_token["client_id"] == oauth_application[oauth_applications_client_id_column]
         | 
| 240 | 
            -
                end
         | 
| 241 | 
            -
             | 
| 242 267 | 
             
                def _jwt_key
         | 
| 243 268 | 
             
                  @_jwt_key ||= oauth_jwt_key || (oauth_application[oauth_applications_client_secret_column] if oauth_application)
         | 
| 244 269 | 
             
                end
         | 
| @@ -412,7 +437,9 @@ module Rodauth | |
| 412 437 | 
             
                  super
         | 
| 413 438 | 
             
                end
         | 
| 414 439 |  | 
| 415 | 
            -
                route(: | 
| 440 | 
            +
                route(:jwks) do |r|
         | 
| 441 | 
            +
                  next unless is_authorization_server?
         | 
| 442 | 
            +
             | 
| 416 443 | 
             
                  r.get do
         | 
| 417 444 | 
             
                    json_response_success({ keys: jwks_set })
         | 
| 418 445 | 
             
                  end
         | 
| @@ -0,0 +1,267 @@ | |
| 1 | 
            +
            # frozen-string-literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Rodauth
         | 
| 4 | 
            +
              Feature.define(:oidc) do
         | 
| 5 | 
            +
                OIDC_SCOPES_MAP = {
         | 
| 6 | 
            +
                  "profile" => %i[name family_name given_name middle_name nickname preferred_username
         | 
| 7 | 
            +
                                  profile picture website gender birthdate zoneinfo locale updated_at].freeze,
         | 
| 8 | 
            +
                  "email" => %i[email email_verified].freeze,
         | 
| 9 | 
            +
                  "address" => %i[address].freeze,
         | 
| 10 | 
            +
                  "phone" => %i[phone_number phone_number_verified].freeze
         | 
| 11 | 
            +
                }.freeze
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                depends :oauth_jwt
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                auth_value_method :oauth_application_default_scope, "openid"
         | 
| 16 | 
            +
                auth_value_method :oauth_application_scopes, %w[openid]
         | 
| 17 | 
            +
             | 
| 18 | 
            +
                auth_value_method :oauth_grants_nonce_column, :nonce
         | 
| 19 | 
            +
                auth_value_method :oauth_tokens_nonce_column, :nonce
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                auth_value_method :invalid_scope_message, "The Access Token expired"
         | 
| 22 | 
            +
             | 
| 23 | 
            +
                auth_value_method :webfinger_relation, "http://openid.net/specs/connect/1.0/issuer"
         | 
| 24 | 
            +
             | 
| 25 | 
            +
                auth_value_methods(:get_oidc_param)
         | 
| 26 | 
            +
             | 
| 27 | 
            +
                def openid_configuration(issuer = nil)
         | 
| 28 | 
            +
                  request.on(".well-known/openid-configuration") do
         | 
| 29 | 
            +
                    request.get do
         | 
| 30 | 
            +
                      json_response_success(openid_configuration_body(issuer))
         | 
| 31 | 
            +
                    end
         | 
| 32 | 
            +
                  end
         | 
| 33 | 
            +
                end
         | 
| 34 | 
            +
             | 
| 35 | 
            +
                def webfinger
         | 
| 36 | 
            +
                  request.on(".well-known/webfinger") do
         | 
| 37 | 
            +
                    request.get do
         | 
| 38 | 
            +
                      resource = param_or_nil("resource")
         | 
| 39 | 
            +
             | 
| 40 | 
            +
                      throw_json_response_error(400, "invalid_request") unless resource
         | 
| 41 | 
            +
             | 
| 42 | 
            +
                      response.status = 200
         | 
| 43 | 
            +
                      response["Content-Type"] ||= "application/jrd+json"
         | 
| 44 | 
            +
             | 
| 45 | 
            +
                      json_payload = JSON.dump({
         | 
| 46 | 
            +
                                                 subject: resource,
         | 
| 47 | 
            +
                                                 links: [{
         | 
| 48 | 
            +
                                                   rel: webfinger_relation,
         | 
| 49 | 
            +
                                                   href: authorization_server_url
         | 
| 50 | 
            +
                                                 }]
         | 
| 51 | 
            +
                                               })
         | 
| 52 | 
            +
                      response.write(json_payload)
         | 
| 53 | 
            +
                      request.halt
         | 
| 54 | 
            +
                    end
         | 
| 55 | 
            +
                  end
         | 
| 56 | 
            +
                end
         | 
| 57 | 
            +
             | 
| 58 | 
            +
                private
         | 
| 59 | 
            +
             | 
| 60 | 
            +
                def create_oauth_grant(create_params = {})
         | 
| 61 | 
            +
                  return super unless (nonce = param_or_nil("nonce"))
         | 
| 62 | 
            +
             | 
| 63 | 
            +
                  super(oauth_grants_nonce_column => nonce)
         | 
| 64 | 
            +
                end
         | 
| 65 | 
            +
             | 
| 66 | 
            +
                def create_oauth_token_from_authorization_code(oauth_grant, create_params)
         | 
| 67 | 
            +
                  return super unless oauth_grant[oauth_grants_nonce_column]
         | 
| 68 | 
            +
             | 
| 69 | 
            +
                  super(oauth_grant, create_params.merge(oauth_tokens_nonce_column => oauth_grant[oauth_grants_nonce_column]))
         | 
| 70 | 
            +
                end
         | 
| 71 | 
            +
             | 
| 72 | 
            +
                def create_oauth_token
         | 
| 73 | 
            +
                  oauth_token = super
         | 
| 74 | 
            +
                  generate_id_token(oauth_token)
         | 
| 75 | 
            +
                  oauth_token
         | 
| 76 | 
            +
                end
         | 
| 77 | 
            +
             | 
| 78 | 
            +
                def generate_id_token(oauth_token)
         | 
| 79 | 
            +
                  oauth_scopes = oauth_token[oauth_tokens_scopes_column].split(oauth_scope_separator)
         | 
| 80 | 
            +
             | 
| 81 | 
            +
                  return unless oauth_scopes.include?("openid")
         | 
| 82 | 
            +
             | 
| 83 | 
            +
                  id_token_claims = jwt_claims(oauth_token)
         | 
| 84 | 
            +
                  id_token_claims[:nonce] = oauth_token[oauth_tokens_nonce_column] if oauth_token[oauth_tokens_nonce_column]
         | 
| 85 | 
            +
             | 
| 86 | 
            +
                  # Time when the End-User authentication occurred.
         | 
| 87 | 
            +
                  #
         | 
| 88 | 
            +
                  # Sounds like the same as issued at claim.
         | 
| 89 | 
            +
                  id_token_claims[:auth_time] = id_token_claims[:iat]
         | 
| 90 | 
            +
             | 
| 91 | 
            +
                  account = db[accounts_table].where(account_id_column => oauth_token[oauth_tokens_account_id_column]).first
         | 
| 92 | 
            +
             | 
| 93 | 
            +
                  # this should never happen!
         | 
| 94 | 
            +
                  # a newly minted oauth token from a grant should have been assigned to an account
         | 
| 95 | 
            +
                  # who just authorized its generation.
         | 
| 96 | 
            +
                  return unless account
         | 
| 97 | 
            +
             | 
| 98 | 
            +
                  fill_with_account_claims(id_token_claims, account, oauth_scopes)
         | 
| 99 | 
            +
             | 
| 100 | 
            +
                  oauth_token[:id_token] = jwt_encode(id_token_claims)
         | 
| 101 | 
            +
                end
         | 
| 102 | 
            +
             | 
| 103 | 
            +
                def fill_with_account_claims(claims, account, scopes)
         | 
| 104 | 
            +
                  scopes_by_oidc = scopes.each_with_object({}) do |scope, by_oidc|
         | 
| 105 | 
            +
                    oidc, param = scope.split(".", 2)
         | 
| 106 | 
            +
             | 
| 107 | 
            +
                    by_oidc[oidc] ||= []
         | 
| 108 | 
            +
             | 
| 109 | 
            +
                    by_oidc[oidc] << param.to_sym if param
         | 
| 110 | 
            +
                  end
         | 
| 111 | 
            +
             | 
| 112 | 
            +
                  oidc_scopes = (OIDC_SCOPES_MAP.keys & scopes_by_oidc.keys)
         | 
| 113 | 
            +
             | 
| 114 | 
            +
                  return if oidc_scopes.empty?
         | 
| 115 | 
            +
             | 
| 116 | 
            +
                  if respond_to?(:get_oidc_param)
         | 
| 117 | 
            +
                    oidc_scopes.each do |scope|
         | 
| 118 | 
            +
                      params = scopes_by_oidc[scope]
         | 
| 119 | 
            +
                      params = params.empty? ? OIDC_SCOPES_MAP[scope] : (OIDC_SCOPES_MAP[scope] & params)
         | 
| 120 | 
            +
             | 
| 121 | 
            +
                      params.each do |param|
         | 
| 122 | 
            +
                        claims[param] = __send__(:get_oidc_param, account, param)
         | 
| 123 | 
            +
                      end
         | 
| 124 | 
            +
                    end
         | 
| 125 | 
            +
                  else
         | 
| 126 | 
            +
                    warn "`get_oidc_param(token, param)` must be implemented to use oidc scopes."
         | 
| 127 | 
            +
                  end
         | 
| 128 | 
            +
                end
         | 
| 129 | 
            +
             | 
| 130 | 
            +
                def json_access_token_payload(oauth_token)
         | 
| 131 | 
            +
                  payload = super
         | 
| 132 | 
            +
                  payload["id_token"] = oauth_token[:id_token] if oauth_token[:id_token]
         | 
| 133 | 
            +
                  payload
         | 
| 134 | 
            +
                end
         | 
| 135 | 
            +
             | 
| 136 | 
            +
                # Authorize
         | 
| 137 | 
            +
             | 
| 138 | 
            +
                def check_valid_response_type?
         | 
| 139 | 
            +
                  case param_or_nil("response_type")
         | 
| 140 | 
            +
                  when "none", "id_token",
         | 
| 141 | 
            +
                       "code token", "code id_token", "id_token token", "code id_token token" # multiple
         | 
| 142 | 
            +
                    true
         | 
| 143 | 
            +
                  else
         | 
| 144 | 
            +
                    super
         | 
| 145 | 
            +
                  end
         | 
| 146 | 
            +
                end
         | 
| 147 | 
            +
             | 
| 148 | 
            +
                def do_authorize(redirect_url, query_params = [], fragment_params = [])
         | 
| 149 | 
            +
                  return super unless use_oauth_implicit_grant_type?
         | 
| 150 | 
            +
             | 
| 151 | 
            +
                  case param("response_type")
         | 
| 152 | 
            +
                  when "id_token"
         | 
| 153 | 
            +
                    fragment_params.replace(_do_authorize_id_token.map { |k, v| "#{k}=#{v}" })
         | 
| 154 | 
            +
                  when "code token"
         | 
| 155 | 
            +
                    redirect_response_error("invalid_request") unless use_oauth_implicit_grant_type?
         | 
| 156 | 
            +
             | 
| 157 | 
            +
                    params = _do_authorize_code.merge(_do_authorize_token)
         | 
| 158 | 
            +
             | 
| 159 | 
            +
                    fragment_params.replace(params.map { |k, v| "#{k}=#{v}" })
         | 
| 160 | 
            +
                  when "code id_token"
         | 
| 161 | 
            +
                    params = _do_authorize_code.merge(_do_authorize_id_token)
         | 
| 162 | 
            +
             | 
| 163 | 
            +
                    fragment_params.replace(params.map { |k, v| "#{k}=#{v}" })
         | 
| 164 | 
            +
                  when "id_token token"
         | 
| 165 | 
            +
                    params = _do_authorize_id_token.merge(_do_authorize_token)
         | 
| 166 | 
            +
             | 
| 167 | 
            +
                    fragment_params.replace(params.map { |k, v| "#{k}=#{v}" })
         | 
| 168 | 
            +
                  when "code id_token token"
         | 
| 169 | 
            +
                    params = _do_authorize_code.merge(_do_authorize_id_token).merge(_do_authorize_token)
         | 
| 170 | 
            +
             | 
| 171 | 
            +
                    fragment_params.replace(params.map { |k, v| "#{k}=#{v}" })
         | 
| 172 | 
            +
                  end
         | 
| 173 | 
            +
             | 
| 174 | 
            +
                  super(redirect_url, query_params, fragment_params)
         | 
| 175 | 
            +
                end
         | 
| 176 | 
            +
             | 
| 177 | 
            +
                def _do_authorize_id_token
         | 
| 178 | 
            +
                  create_params = {
         | 
| 179 | 
            +
                    oauth_tokens_account_id_column => account_id,
         | 
| 180 | 
            +
                    oauth_tokens_oauth_application_id_column => oauth_application[oauth_applications_id_column],
         | 
| 181 | 
            +
                    oauth_tokens_scopes_column => scopes
         | 
| 182 | 
            +
                  }
         | 
| 183 | 
            +
                  oauth_token = generate_oauth_token(create_params, false)
         | 
| 184 | 
            +
                  generate_id_token(oauth_token)
         | 
| 185 | 
            +
                  params = json_access_token_payload(oauth_token)
         | 
| 186 | 
            +
                  params.delete("access_token")
         | 
| 187 | 
            +
                  params
         | 
| 188 | 
            +
                end
         | 
| 189 | 
            +
             | 
| 190 | 
            +
                # Metadata
         | 
| 191 | 
            +
             | 
| 192 | 
            +
                def openid_configuration_body(path)
         | 
| 193 | 
            +
                  metadata = oauth_server_metadata_body(path)
         | 
| 194 | 
            +
             | 
| 195 | 
            +
                  scope_claims = oauth_application_scopes.each_with_object([]) do |scope, claims|
         | 
| 196 | 
            +
                    oidc, param = scope.split(".", 2)
         | 
| 197 | 
            +
                    if param
         | 
| 198 | 
            +
                      claims << param
         | 
| 199 | 
            +
                    else
         | 
| 200 | 
            +
                      oidc_claims = OIDC_SCOPES_MAP[oidc]
         | 
| 201 | 
            +
                      claims.concat(oidc_claims) if oidc_claims
         | 
| 202 | 
            +
                    end
         | 
| 203 | 
            +
                  end
         | 
| 204 | 
            +
             | 
| 205 | 
            +
                  scope_claims.unshift("auth_time") if last_account_login_at
         | 
| 206 | 
            +
             | 
| 207 | 
            +
                  metadata.merge({
         | 
| 208 | 
            +
                                   userinfo_endpoint: userinfo_url,
         | 
| 209 | 
            +
                                   response_types_supported: metadata[:response_types_supported] +
         | 
| 210 | 
            +
                                     ["none", "id_token", %w[code token], %w[code id_token], %w[id_token token], %w[code id_token token]],
         | 
| 211 | 
            +
                                   response_modes_supported: %w[query fragment],
         | 
| 212 | 
            +
                                   grant_types_supported: %w[authorization_code implicit],
         | 
| 213 | 
            +
             | 
| 214 | 
            +
                                   subject_types_supported: [oauth_jwt_subject_type],
         | 
| 215 | 
            +
             | 
| 216 | 
            +
                                   id_token_signing_alg_values_supported: metadata[:token_endpoint_auth_signing_alg_values_supported],
         | 
| 217 | 
            +
                                   id_token_encryption_alg_values_supported: [oauth_jwt_jwe_algorithm].compact,
         | 
| 218 | 
            +
                                   id_token_encryption_enc_values_supported: [oauth_jwt_jwe_encryption_method].compact,
         | 
| 219 | 
            +
             | 
| 220 | 
            +
                                   userinfo_signing_alg_values_supported: [],
         | 
| 221 | 
            +
                                   userinfo_encryption_alg_values_supported: [],
         | 
| 222 | 
            +
                                   userinfo_encryption_enc_values_supported: [],
         | 
| 223 | 
            +
             | 
| 224 | 
            +
                                   request_object_signing_alg_values_supported: [],
         | 
| 225 | 
            +
                                   request_object_encryption_alg_values_supported: [],
         | 
| 226 | 
            +
                                   request_object_encryption_enc_values_supported: [],
         | 
| 227 | 
            +
             | 
| 228 | 
            +
                                   # These Claim Types are described in Section 5.6 of OpenID Connect Core 1.0 [OpenID.Core].
         | 
| 229 | 
            +
                                   # Values defined by this specification are normal, aggregated, and distributed.
         | 
| 230 | 
            +
                                   # If omitted, the implementation supports only normal Claims.
         | 
| 231 | 
            +
                                   claim_types_supported: %w[normal],
         | 
| 232 | 
            +
                                   claims_supported: %w[sub iss iat exp aud] | scope_claims
         | 
| 233 | 
            +
                                 })
         | 
| 234 | 
            +
                end
         | 
| 235 | 
            +
             | 
| 236 | 
            +
                # /userinfo
         | 
| 237 | 
            +
                route(:userinfo) do |r|
         | 
| 238 | 
            +
                  next unless is_authorization_server?
         | 
| 239 | 
            +
             | 
| 240 | 
            +
                  r.on method: %i[get post] do
         | 
| 241 | 
            +
                    catch_error do
         | 
| 242 | 
            +
                      oauth_token = authorization_token
         | 
| 243 | 
            +
             | 
| 244 | 
            +
                      throw_json_response_error(authorization_required_error_status, "invalid_token") unless oauth_token
         | 
| 245 | 
            +
             | 
| 246 | 
            +
                      oauth_scopes = oauth_token["scope"].split(" ")
         | 
| 247 | 
            +
             | 
| 248 | 
            +
                      throw_json_response_error(authorization_required_error_status, "invalid_token") unless oauth_scopes.include?("openid")
         | 
| 249 | 
            +
             | 
| 250 | 
            +
                      account = db[accounts_table].where(account_id_column => oauth_token["sub"]).first
         | 
| 251 | 
            +
             | 
| 252 | 
            +
                      throw_json_response_error(authorization_required_error_status, "invalid_token") unless account
         | 
| 253 | 
            +
             | 
| 254 | 
            +
                      oauth_scopes.delete("openid")
         | 
| 255 | 
            +
             | 
| 256 | 
            +
                      oidc_claims = { "sub" => oauth_token["sub"] }
         | 
| 257 | 
            +
             | 
| 258 | 
            +
                      fill_with_account_claims(oidc_claims, account, oauth_scopes)
         | 
| 259 | 
            +
             | 
| 260 | 
            +
                      json_response_success(oidc_claims)
         | 
| 261 | 
            +
                    end
         | 
| 262 | 
            +
             | 
| 263 | 
            +
                    throw_json_response_error(authorization_required_error_status, "invalid_token")
         | 
| 264 | 
            +
                  end
         | 
| 265 | 
            +
                end
         | 
| 266 | 
            +
              end
         | 
| 267 | 
            +
            end
         | 
    
        metadata
    CHANGED
    
    | @@ -1,14 +1,14 @@ | |
| 1 1 | 
             
            --- !ruby/object:Gem::Specification
         | 
| 2 2 | 
             
            name: rodauth-oauth
         | 
| 3 3 | 
             
            version: !ruby/object:Gem::Version
         | 
| 4 | 
            -
              version: 0.0 | 
| 4 | 
            +
              version: 0.1.0
         | 
| 5 5 | 
             
            platform: ruby
         | 
| 6 6 | 
             
            authors:
         | 
| 7 7 | 
             
            - Tiago Cardoso
         | 
| 8 | 
            -
            autorequire: | 
| 8 | 
            +
            autorequire:
         | 
| 9 9 | 
             
            bindir: bin
         | 
| 10 10 | 
             
            cert_chain: []
         | 
| 11 | 
            -
            date: 2020- | 
| 11 | 
            +
            date: 2020-08-01 00:00:00.000000000 Z
         | 
| 12 12 | 
             
            dependencies: []
         | 
| 13 13 | 
             
            description: Implementation of the OAuth 2.0 protocol on top of rodauth.
         | 
| 14 14 | 
             
            email:
         | 
| @@ -32,6 +32,7 @@ files: | |
| 32 32 | 
             
            - lib/rodauth/features/oauth.rb
         | 
| 33 33 | 
             
            - lib/rodauth/features/oauth_http_mac.rb
         | 
| 34 34 | 
             
            - lib/rodauth/features/oauth_jwt.rb
         | 
| 35 | 
            +
            - lib/rodauth/features/oidc.rb
         | 
| 35 36 | 
             
            - lib/rodauth/oauth.rb
         | 
| 36 37 | 
             
            - lib/rodauth/oauth/railtie.rb
         | 
| 37 38 | 
             
            - lib/rodauth/oauth/ttl_store.rb
         | 
| @@ -42,7 +43,7 @@ metadata: | |
| 42 43 | 
             
              homepage_uri: https://gitlab.com/honeyryderchuck/roda-oauth
         | 
| 43 44 | 
             
              source_code_uri: https://gitlab.com/honeyryderchuck/roda-oauth
         | 
| 44 45 | 
             
              changelog_uri: https://gitlab.com/honeyryderchuck/roda-oauth/-/blob/master/CHANGELOG.md
         | 
| 45 | 
            -
            post_install_message: | 
| 46 | 
            +
            post_install_message:
         | 
| 46 47 | 
             
            rdoc_options: []
         | 
| 47 48 | 
             
            require_paths:
         | 
| 48 49 | 
             
            - lib
         | 
| @@ -58,7 +59,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement | |
| 58 59 | 
             
                  version: '0'
         | 
| 59 60 | 
             
            requirements: []
         | 
| 60 61 | 
             
            rubygems_version: 3.1.2
         | 
| 61 | 
            -
            signing_key: | 
| 62 | 
            +
            signing_key:
         | 
| 62 63 | 
             
            specification_version: 4
         | 
| 63 64 | 
             
            summary: Implementation of the OAuth 2.0 protocol on top of rodauth.
         | 
| 64 65 | 
             
            test_files: []
         |