Skip to content

Doorkeeper OIDC prototype#12

Draft
rochlefebvre wants to merge 7 commits intomasterfrom
doorkeeper-oidc-prototype
Draft

Doorkeeper OIDC prototype#12
rochlefebvre wants to merge 7 commits intomasterfrom
doorkeeper-oidc-prototype

Conversation

@rochlefebvre
Copy link
Copy Markdown

@rochlefebvre rochlefebvre commented Feb 4, 2022

Work in progress

A proof of concept branch that adds enough support for OAuth and OpenID Connect to work with sigstore's auth and Fulcio components (I think).

Note that I manually registered an application client using the Rails console. Imagine that sigstore would be registered as a client capable of maintaining a client secret.

Doorkeeper::Application.create(name: "MyApp", redirect_uri: "http://localhost:5678", scopes: ["openid"])
#<Doorkeeper::Application:0x000000013543dc90
 id: 1,
 name: "MyApp",
 uid: "-lFGlvO_I6KW7ktJfRUJH0jGP2iWs174loxTOrtmOg0",
 secret: "GhGTn0tLFWbApGyJJYYo1e_6uTyTrreCWY7bZMyctRQ",
 redirect_uri: "http://localhost:5678",
 scopes: "openid",
 confidential: true,
 created_at: Fri, 04 Feb 2022 17:25:02.554234000 UTC +00:00,
 updated_at: Fri, 04 Feb 2022 17:25:02.554234000 UTC +00:00>

Sending a request for an authorization code:
Edit: ignore the PKCE fields in my examples and in the code. I don't think that will be needed, given that sigstore is a server application that can maintain a client_secret.

Started GET "/oauth/authorize
?client_id=lFGlvO_I6KW7ktJfRUJH0jGP2iWs174loxTOrtmOg0
&code_challenge=FENDoRhPCErBjfjR4qYiFu_njDbgy5qwk39p1DfLHqM
&code_challenge_method=S256
&nonce=cecea7452701d0c219e8aeadfa5caa7f
&redirect_uri=http%3A%2F%2Flocalhost%3A5678
&response_type=code
&scope=openid
&state=1054a1ca7b287a618024db204ebf7ace"

Normally we would be redirected to the sign in page as needed. We would need to grant MyApp the openid scope, unless we decide to auto-grant. An explicit grant step seems a bit irrelevant in the sigstore usecase, IMO.

The server redirects the browser to the redirect_uri with the auth code and ephemeral state

Redirected to http://localhost:5678?code=OOk_F6siO7_VOtJVXbLm4b5r9pkAl_lSV5-fGSaVXvM&state=1054a1ca7b287a618024db204ebf7ace
Completed 302

The client then POSTs to the token endpoint, supplying the auth code along with other expected params:

Started POST "/oauth/token" for ::1 at 2022-02-04 15:55:40 -0500
Processing by Doorkeeper::TokensController#create as */*
  Parameters: {"grant_type"=>"authorization_code", "client_id"=>"-lFGlvO_I6KW7ktJfRUJH0jGP2iWs174loxTOrtmOg0", "client_secret"=>"[FILTERED]", "redirect_uri"=>"http://localhost:5678", "code"=>"[FILTERED]", "code_verifier"=>"43309e4f4c41469e4c1cd7ea568d018897f2c4675670cf9c" [...]}
...
Completed 200 OK in 55ms

The client receives this response

{
    "access_token": "PqkQyLLT6yab8hzi2fuu8XbEUmJ5KRUPZg6K7Mipuys",
    "token_type": "Bearer",
    "expires_in": 7200,
    "scope": "openid",
    "created_at": 1644008140,
    "id_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6Ii1hZFpnLXJKVFFBSlRXYmhfUjlBci1WUGRNUjNSejZ0VHQ4Z045ek11M1kifQ.eyJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjMwMDAvb2F1dGgiLCJzdWIiOiJodHRwOi8vbG9jYWxob3N0OjMwMDAvcHJvZmlsZXMvTGFSb2NhaWxsZSIsImF1ZCI6Ii1sRkdsdk9fSTZLVzdrdEpmUlVKSDBqR1AyaVdzMTc0bG94VE9ydG1PZzAiLCJleHAiOjE2NDQwMDgyNjAsImlhdCI6MTY0NDAwODE0MCwibm9uY2UiOiI5MWZhMzE3NDg0ZTE3MzkxN2UzZGQ5MjRlOTY1ZWVmMSJ9.TFjov2mOpurmP7-7hi7Wxy-ZVtGoagTNOSvk-KMywglZsOIkR7ZK9VDml_2qABfE8U4OwttmHhSkLFW9gGyV2P6c18xpJgpf8-cbAk95QfFHpmBfngegwvtwZi8USNx1IoKVH2GonYpZCCgn2WswJSohMfnKJTgQvYhDqiG7DXHOQex0Rd8iAtWCNA4ScPt0fmoqfRXn-HdPOU0_KfKu4_-9Z74MlkPHi62gdzxtpOLL927tmSePGzb9n371kLL8i7ZlXEEMOpbeKGfjlbQE4HisWWxZ0nxD-4cCWW47xV-y_4yCTlAE4mo4oseo52ux_6AUEtHPp6DflSSImDua8w"
}

The decoded id_token contains the rubygems.org account's profile URI:

# header
{
  "typ": "JWT",
  "alg": "RS256",
  "kid": "-adZg-rJTQAJTWbh_R9Ar-VPdMR3Rz6tTt8gN9zMu3Y"
}
# payload
{
  "iss": "http://localhost:3000/oauth",
  "sub": "http://localhost:3000/profiles/LaRocaille",
  "aud": "-lFGlvO_I6KW7ktJfRUJH0jGP2iWs174loxTOrtmOg0",
  "exp": 1644008178,
  "iat": 1644008058,
  "nonce": "cecea7452701d0c219e8aeadfa5caa7f"
}

Sample response from http://localhost:3000/.well-known/openid-configuration

{
    "issuer": {},
    "authorization_endpoint": "http://localhost:3000/oauth/authorize",
    "token_endpoint": "http://localhost:3000/oauth/token",
    "revocation_endpoint": "http://localhost:3000/oauth/revoke",
    "introspection_endpoint": "http://localhost:3000/oauth/introspect",
    "userinfo_endpoint": "http://localhost:3000/oauth/userinfo",
    "jwks_uri": "http://localhost:3000/oauth/discovery/keys",
    "scopes_supported": [],
    "response_types_supported": [
        "code",
        "token",
        "id_token",
        "id_token token"
    ],
    "response_modes_supported": [
        "query",
        "fragment",
        "form_post"
    ],
    "grant_types_supported": [
        "authorization_code",
        "implicit_oidc"
    ],
    "token_endpoint_auth_methods_supported": [
        "client_secret_basic",
        "client_secret_post"
    ],
    "subject_types_supported": [
        "public"
    ],
    "id_token_signing_alg_values_supported": [
        "RS256"
    ],
    "claim_types_supported": [
        "normal"
    ],
    "claims_supported": [
        "iss",
        "sub",
        "aud",
        "exp",
        "iat"
    ]
}

I did not spend much time configuring OpenID. Our config could go on a diet!

Sample response http://localhost:3000/oauth/discovery/keys

{
    "keys": [
        {
            "kty": "RSA",
            "kid": "-adZg-rJTQAJTWbh_R9Ar-VPdMR3Rz6tTt8gN9zMu3Y",
            "e": "AQAB",
            "n": "2cfIXkL6aYBETmJOHDwvtN-uls3ayCRILF0VfmEonkytaZnUcK4UP7QVuvqdbZiRUzOx0HroQYyoflHPhZtHzwgQl2wYC8qfPp3lmljydh9w9yO0kBMgQbuUl0YxemqrZCuTaW0mZtvYFp5daBflvBcnHwNVN5kRkMdR6VzP8vgfChjDzaNBMlPVvAfx5DFKjnvinxZoCuL2dEZS9pWcp6pGGO2ZTXPumBh9pQsooLwxcN0AYo0ZN1z3ZeKtQuH9hxhFWr-bNvEFWgx5d8nx1UFl6D3AySkin4LmJVBcqZ7VHiHxbc4jVH64-d8oWs3DgvYW8iq9I6EP988uviMUEw",
            "use": "sig",
            "alg": "RS256"
        }
    ]
}

<%= hidden_field_tag :response_type, @pre_auth.response_type %>
<%= hidden_field_tag :response_mode, @pre_auth.response_mode %>
<%= hidden_field_tag :scope, @pre_auth.scope %>
<%= hidden_field_tag :nonce, @pre_auth.nonce %>
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

has_many :ownership_calls, -> { opened }, dependent: :destroy, inverse_of: :user
has_many :ownership_requests, -> { opened }, dependent: :destroy, inverse_of: :user

has_many :access_grants, class_name: 'Doorkeeper::AccessGrant', foreign_key: :resource_owner_id, dependent: :destroy
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

# Example implementation:
# User.find_by(id: session[:user_id]) || redirect_to(new_user_session_url)

user = User.last
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Terrible hack! I'm not familiar enough with rubygems.org and its use of Clearance to find the currently authenticated user.

See https://doorkeeper.gitbook.io/guides/ruby-on-rails/configuration#resource-owner-authentication


Doorkeeper::OpenidConnect.configure do
issuer do |resource_owner, application|
"http://localhost:3000/oauth"
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO compare this with other OIDC provider issuer urls.

"http://localhost:3000/oauth"
end

signing_key File.read("private_id_token_key.pem")
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

# or if you need pairwise subject identifier, implement like below:
# Digest::SHA256.hexdigest("#{resource_owner.id}#{URI.parse(application.redirect_uri).host}#{'your_secret_salt'}")
# profile_path(resource_owner.display_id)
"http://localhost:3000/profiles/#{resource_owner.display_id}"
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This sets the sub claim in the ID token. Fulcio may prefer to receive the subject alternative name URI from another claim.

@@ -1,4 +1,6 @@
Rails.application.routes.draw do
use_doorkeeper_openid_connect
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We may not need all routes and actions. There is room for improvement here.

@@ -0,0 +1,8 @@
# frozen_string_literal: true

class EnablePkce < ActiveRecord::Migration[6.1]
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On further reflection, PKCE likely will not be needed for requests coming from sigstore. It is a server-side application, and can be trusted with a client secret.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant