Class: Persona::Oidc::Verifier

Inherits:
Object
  • Object
show all
Defined in:
lib/persona/oidc/verifier.rb

Overview

Production OIDC verifier provider (docs/spec §7, P-16): the authorization-code + PKCE flow, OIDC discovery, code exchange, and id_token validation against the IdP's published JWKS. Mirrors the TypeScript createOidcVerifierProvider.

Signature verification uses the jwt gem's OpenSSL backend and covers RS256 and ES256. EdDSA (Ed25519) is intentionally not enabled here — it needs rbnacl in Ruby — so an IdP signing with EdDSA must expose an RS256/ES256 key or wait for that follow-up.

The clock (#now) is injectable so expiry is checked here rather than by the jwt gem, matching the TS verifier and keeping tests deterministic.

Constant Summary collapse

ALLOWED_ALGS =
%w[RS256 ES256].freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(name:, issuer:, client_id:, redirect_uri:, client_secret: nil, scope: 'openid email', authorization_endpoint: nil, token_endpoint: nil, jwks_uri: nil, authorize_params: {}, require_hosted_domain: nil, allowed_email_domain: nil, http: NetHttpClient.new, now: -> { Time.now.to_i }, clock_tolerance: 60) ⇒ Verifier

Returns a new instance of Verifier.



53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
# File 'lib/persona/oidc/verifier.rb', line 53

def initialize(
  name:, issuer:, client_id:, redirect_uri:,
  client_secret: nil, scope: 'openid email',
  authorization_endpoint: nil, token_endpoint: nil, jwks_uri: nil,
  authorize_params: {}, require_hosted_domain: nil, allowed_email_domain: nil,
  http: NetHttpClient.new, now: -> { Time.now.to_i }, clock_tolerance: 60
)
  @name = name
  @issuer = issuer.to_s.sub(%r{/\z}, '')
  @client_id = client_id
  @redirect_uri = redirect_uri
  @client_secret = client_secret
  @scope = scope
  @explicit_endpoints = { authorization_endpoint: authorization_endpoint,
                          token_endpoint: token_endpoint, jwks_uri: jwks_uri }
  @authorize_params = authorize_params
  @require_hosted_domain = require_hosted_domain
  @allowed_email_domain = allowed_email_domain
  @http = http
  @now = now
  @clock_tolerance = clock_tolerance
  @discovery = nil
  @jwks = nil
end

Instance Attribute Details

#nameObject (readonly)

Returns the value of attribute name.



51
52
53
# File 'lib/persona/oidc/verifier.rb', line 51

def name
  @name
end

Class Method Details

.uniba_auth(issuer:, client_id:, redirect_uri:, client_secret: nil, allowed_email_domain: 'uniba.jp', http: NetHttpClient.new, now: -> { Time.now.to_i }) ⇒ Object

Preset for uniba/auth (github.com/uniba/auth): the uniba.jp shared OIDC AS. Fills in the name, scope, and hd=uniba.jp upstream constraint; the issuer and client credentials stay per-deployment. auth is not yet published, so the token-claim domain check defaults to the email domain (provisional). Pass allowed_email_domain: nil to disable it.



83
84
85
86
87
88
89
90
91
92
# File 'lib/persona/oidc/verifier.rb', line 83

def self.uniba_auth(issuer:, client_id:, redirect_uri:, client_secret: nil,
                    allowed_email_domain: 'uniba.jp',
                    http: NetHttpClient.new, now: -> { Time.now.to_i })
  new(
    name: 'uniba-auth', issuer: issuer, client_id: client_id, redirect_uri: redirect_uri,
    client_secret: client_secret, scope: 'openid email',
    authorize_params: { hd: 'uniba.jp' }, allowed_email_domain: allowed_email_domain,
    http: http, now: now,
  )
end

Instance Method Details

#authorize(state) ⇒ Object



94
95
96
97
98
99
100
101
102
103
104
105
# File 'lib/persona/oidc/verifier.rb', line 94

def authorize(state)
  code_verifier = SecureRandom.urlsafe_base64(32)
  nonce = SecureRandom.urlsafe_base64(16)
  query = {
    response_type: 'code', client_id: @client_id, redirect_uri: @redirect_uri,
    scope: @scope, state: state, nonce: nonce,
    code_challenge: base64url(Digest::SHA256.digest(code_verifier)),
    code_challenge_method: 'S256',
  }.merge(@authorize_params)
  url = "#{discovery.fetch(:authorization_endpoint)}?#{URI.encode_www_form(query)}"
  AuthorizeStart.new(url: url, stash: { code_verifier: code_verifier, nonce: nonce })
end

#verify(params, stash = nil) ⇒ Object



107
108
109
110
111
112
113
114
115
116
# File 'lib/persona/oidc/verifier.rb', line 107

def verify(params, stash = nil)
  code = params[:code] || params['code']
  return nil if code.nil? || code.to_s.empty?

  tokens = exchange_code(code, stash)
  id_token = tokens && (tokens['id_token'] || tokens[:id_token])
  return nil if id_token.nil?

  validate_id_token(id_token, stash)
end