CI / Test (pull_request) Failing after 46s
- Make authorization-code redemption atomic: claim the code with a guarded delete inside the transaction that creates the authorization, so concurrent exchanges can't both issue a token (RFC 6749 4.1.3) - Compare the token request's client_id host to the code's stored client_id (defense-in-depth alongside the exact redirect_uri match) - Reject unknown response_type values with unsupported_response_type - Validate the PKCE code_challenge (43-128 unreserved chars, S256) so a client can't persist an over-long or malformed challenge - Return invalid_request for missing token-endpoint parameters, and set Cache-Control: no-store on error responses too - Use the right separator when the redirect_uri already has a query string - Look codes up through the active scope; drop the unused consume!; serialize permissions as JSON - Purge abandoned authorization codes via a recurring Solid Queue task Part of #254
47 lines
1.6 KiB
Ruby
47 lines
1.6 KiB
Ruby
module RsOauthManager
|
|
class ExchangeCode < RsOauthManagerService
|
|
def initialize(code:, client_id:, redirect_uri:, code_verifier:)
|
|
@code = code
|
|
@client_id = client_id
|
|
@redirect_uri = redirect_uri
|
|
@code_verifier = code_verifier
|
|
end
|
|
|
|
def call
|
|
auth_code = RsAuthorizationCode.find_by_code(@code)
|
|
raise InvalidGrant if auth_code.nil?
|
|
|
|
raise InvalidGrant unless hostname_of(@client_id) == hostname_of(@redirect_uri)
|
|
raise InvalidGrant unless hostname_of(@client_id) == hostname_of(auth_code.client_id)
|
|
raise InvalidGrant unless @redirect_uri == auth_code.redirect_uri
|
|
|
|
raise InvalidGrant unless VerifyPkce.call(
|
|
code_challenge: auth_code.code_challenge,
|
|
code_challenge_method: auth_code.code_challenge_method,
|
|
code_verifier: @code_verifier
|
|
)
|
|
|
|
user = auth_code.user
|
|
auth = nil
|
|
|
|
# Claim the code atomically so a concurrent redemption can't issue a
|
|
# second token. The whole exchange is one transaction: if creating the
|
|
# authorization fails, the code is not consumed and can be retried.
|
|
RsAuthorizationCode.transaction do
|
|
claimed = RsAuthorizationCode.where(id: auth_code.id).delete_all
|
|
raise InvalidGrant if claimed != 1
|
|
|
|
auth = user.remote_storage_authorizations.create!(
|
|
permissions: auth_code.permissions,
|
|
client_id: auth_code.client_id,
|
|
redirect_uri: auth_code.redirect_uri,
|
|
app_name: auth_code.client_id,
|
|
expire_at: auth_code.permission_expire_at
|
|
)
|
|
end
|
|
|
|
auth
|
|
end
|
|
end
|
|
end
|