Class: OmniauthOpenidFederation::Jws

Inherits:
Object
  • Object
show all
Defined in:
lib/omniauth_openid_federation/jws.rb

Overview

JWT Request Object builder for signed authorization requests

Examples:

Create and sign a request object with local private key

jws = Jws.new(
  client_id: "client-id",
  redirect_uri: "https://example.com/callback",
  scope: "openid",
  issuer: "https://provider.example.com",
  audience: "https://provider.example.com",
  private_key: private_key,
  key_source: :local
)
signed_jwt = jws.sign

Create and sign a request object with federation/JWKS

jws = Jws.new(
  client_id: "client-id",
  redirect_uri: "https://example.com/callback",
  scope: "openid",
  issuer: "https://provider.example.com",
  audience: "https://provider.example.com",
  private_key: private_key, # Fallback if JWKS not available
  jwks: jwks_hash,
  entity_statement_path: "config/provider-entity-statement.jwt",
  key_source: :federation
)
signed_jwt = jws.sign

Constant Summary collapse

STATE_BYTES =

State generation constants

16

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(client_id:, redirect_uri:, scope: "openid", issuer: nil, audience: nil, state: nil, nonce: nil, response_type: "code", response_mode: nil, login_hint: nil, ui_locales: nil, claims_locales: nil, prompt: nil, hd: nil, acr_values: nil, extra_params: {}, private_key: nil, jwks: nil, entity_statement_path: nil, key_source: :local, client_entity_statement: nil) ⇒ Jws

Initialize JWT request object builder

Parameters:

  • client_id (String)

    OAuth client identifier

  • redirect_uri (String)

    OAuth redirect URI

  • scope (String) (defaults to: "openid")

    OAuth scopes (default: "openid")

  • issuer (String, nil) (defaults to: nil)

    Provider issuer URI

  • audience (String, nil) (defaults to: nil)

    JWT audience (typically provider issuer)

  • state (String, nil) (defaults to: nil)

    CSRF protection state (auto-generated if nil)

  • nonce (String, nil) (defaults to: nil)

    Replay protection nonce

  • response_type (String) (defaults to: "code")

    OAuth response type (default: "code")

  • response_mode (String, nil) (defaults to: nil)

    OAuth response mode

  • login_hint (String, nil) (defaults to: nil)

    Login hint for provider

  • ui_locales (String, nil) (defaults to: nil)

    UI locale preferences

  • claims_locales (String, nil) (defaults to: nil)

    Claims locale preferences

  • prompt (String, nil) (defaults to: nil)

    OAuth prompt parameter

  • hd (String, nil) (defaults to: nil)

    Hosted domain parameter

  • acr_values (String, nil) (defaults to: nil)

    Authentication context class reference values

  • extra_params (Hash) (defaults to: {})

    Additional claims to include in JWT

  • private_key (OpenSSL::PKey::RSA, String, nil) (defaults to: nil)

    Private key for signing (fallback if JWKS not provided)

  • jwks (Hash, Array, nil) (defaults to: nil)

    JWKS hash or array for extracting signing key

  • entity_statement_path (String, nil) (defaults to: nil)

    Path to entity statement file for key extraction (replaces metadata_path)

  • key_source (Symbol) (defaults to: :local)

    Key source: :local (use local static private_key) or :federation (use federation/JWKS)

  • client_entity_statement (String, nil) (defaults to: nil)

    Client's entity statement JWT string (for automatic registration)



86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
# File 'lib/omniauth_openid_federation/jws.rb', line 86

def initialize(
  client_id:,
  redirect_uri:,
  scope: "openid",
  issuer: nil,
  audience: nil,
  state: nil,
  nonce: nil,
  response_type: "code",
  response_mode: nil,
  login_hint: nil,
  ui_locales: nil,
  claims_locales: nil,
  prompt: nil,
  hd: nil,
  acr_values: nil,
  extra_params: {},
  private_key: nil,
  jwks: nil,
  entity_statement_path: nil,
  key_source: :local,
  client_entity_statement: nil
)
  # Security: User input parameters are validated and sanitized before reaching here
  # Configuration parameters are trusted and only trimmed for consistency
  # Store trimmed values to ensure consistency
  @client_id = client_id.to_s.strip
  @redirect_uri = redirect_uri.to_s.strip
  @scope = scope.to_s.strip
  @issuer = issuer&.to_s&.strip
  @audience = audience&.to_s&.strip
  @state = state&.to_s&.strip || SecureRandom.hex(STATE_BYTES)
  @nonce = nonce&.to_s&.strip
  @response_type = response_type.to_s.strip
  @response_mode = response_mode&.to_s&.strip
  # User input parameters (already sanitized)
  @login_hint = &.to_s&.strip
  @ui_locales = ui_locales&.to_s&.strip
  @claims_locales = claims_locales&.to_s&.strip
  # Configuration parameters (trusted, only trimmed)
  @prompt = prompt&.to_s&.strip
  @hd = hd&.to_s&.strip
  # User input parameter (already sanitized)
  @acr_values = acr_values&.to_s&.strip
  @extra_params = extra_params
  @jwks = jwks
  @entity_statement_path = entity_statement_path
  @key_source = key_source
  @client_entity_statement = client_entity_statement

  # Extract signing key based on key_source configuration
  # :local - Use local static private_key directly (for current setup)
  # :federation - Use federation/JWKS from entity statement first, fallback to private_key
  # According to OpenID Federation spec: supports separate signing/encryption keys
  if @key_source == :federation
    # Try federation/JWKS from entity statement first, then fallback to local private_key
     =  if @entity_statement_path
    @private_key = KeyExtractor.extract_signing_key(
      jwks: @jwks,
      metadata: ,
      private_key: private_key
    ) || private_key
  else
    # :local - Use local private_key directly, ignore JWKS/metadata
    @private_key = private_key
  end
end

Instance Attribute Details

#nonceObject

Number of hex bytes for state parameter



61
62
63
# File 'lib/omniauth_openid_federation/jws.rb', line 61

def nonce
  @nonce
end

#private_keyObject

Number of hex bytes for state parameter



61
62
63
# File 'lib/omniauth_openid_federation/jws.rb', line 61

def private_key
  @private_key
end

#stateObject

Number of hex bytes for state parameter



61
62
63
# File 'lib/omniauth_openid_federation/jws.rb', line 61

def state
  @state
end

Instance Method Details

#add_claim(key, value) ⇒ Object

Add a custom claim to the JWT

Parameters:

  • key (Symbol, String)

    Claim key

  • value (Object)

    Claim value



158
159
160
# File 'lib/omniauth_openid_federation/jws.rb', line 158

def add_claim(key, value)
  @extra_params[key] = value
end

#sign(provider_metadata: nil, always_encrypt: false) ⇒ String

Sign the request object JWT

Required for secure authorization requests per RFC 9101. All authentication requests MUST use signed request objects. This method enforces this requirement - unsigned requests are NOT allowed.

According to OpenID Connect Core and RFC 9101, request objects can be:

  • Signed only (default)
  • Signed and encrypted (if provider metadata specifies encryption)

Parameters:

  • provider_metadata (Hash, nil) (defaults to: nil)

    Provider metadata from entity statement (optional)

Returns:

  • (String)

    The signed (and optionally encrypted) JWT request object

Raises:



175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
# File 'lib/omniauth_openid_federation/jws.rb', line 175

def sign(provider_metadata: nil, always_encrypt: false)
  # ENFORCE: Private key is MANDATORY - no bypass possible
  Validators.validate_private_key!(@private_key)

  begin
    signed_jwt = build_jwt
    unless OmniauthOpenidFederation::StringHelpers.present?(signed_jwt)
      error_msg = "Failed to sign JWT request object - signed request objects are MANDATORY"
      OmniauthOpenidFederation::Logger.error("[Jws] #{error_msg}")
      raise SecurityError, error_msg
    end

    # Extract kid from header for logging
    header_part = signed_jwt.split(".").first
    header = JSON.parse(Base64.urlsafe_decode64(header_part))
    kid = header["kid"]
    OmniauthOpenidFederation::Logger.debug("[Jws] Successfully signed request object with kid: #{kid}")

    # Encrypt if required (provider metadata specifies encryption OR always_encrypt option is true)
    # According to RFC 9101 and OpenID Connect Core, if provider specifies
    # request_object_encryption_alg, the client SHOULD encrypt request objects
    if should_encrypt_request_object?(, always_encrypt: always_encrypt)
      encrypted_jwt = encrypt_request_object(signed_jwt, )
      OmniauthOpenidFederation::Logger.debug("[Jws] Successfully encrypted request object")
      encrypted_jwt
    else
      signed_jwt
    end
  rescue => e
    error_msg = "Failed to sign JWT request object (required for secure authorization): #{e.class} - #{e.message}"
    OmniauthOpenidFederation::Logger.error("[Jws] #{error_msg}")
    raise SignatureError, error_msg, e.backtrace
  end
end