Skip to content

Masks::Client

The protocol half of the gem: plain Ruby, no Rails, no database. Every entry point here is a class method that builds one of the objects below.

Which one you want depends on what the application is doing:

  • issuer — an app signing people in — discovery, PKCE, the code exchange

  • verifier — an API checking a bearer it was handed

  • resource — an API publishing what it is and which scopes it takes

  • handshake — an app registering itself, once, without a copied secret

Everything reachable from here talks HTTP to a masks issuer and holds no state of its own beyond the discovery cache in ::registry.

Generated from client/lib by bundle exec rake reference. Its shape is the code’s; the prose is the RDoc in the source.

Inherits Masks::Client::Error.

Attribute Access
code R
description R
scope R
status R
::new(code, description, status:, scope: nil, dpop: false)
#dpop?()
Constant Value
AVATARS "masks:avatars".freeze
ORGANIZATION "org".freeze
ORGANIZATIONS "orgs".freeze
Attribute Access
to_h R
::new(claims)

#[]

#[](name)
#act()
#audience()
#avatars()
#client_id()
#expired?(leeway: 0)
#expires_at()
#issued_at()
#issuer()
#jti()
#member!(*roles, organization: nil)
#organization()
#organizations()
#permit!(scope)
#permits?(scope)
#picture()
#scopes()
#subject()
#tenant()
Constant Value
STYLES %w[photo identicon initials].freeze
FALLBACK "identicon".freeze
Attribute Access
to_h R
::new(hash)
#==(other)
#[](style)
#photo?()
#present?()
Constant Value
OWNER "owner".freeze
Attribute Access
to_h R
::new(hash)
#==(other)
#id()
#key()
#name()
#owner?()
#present?()
#role()
#role?(*roles)
Attribute Access
to_h R
::new(hash)
#==(other)
#name()
#present?()
#subdomain()
#uuid()
Constant Value
SCOPE "masks:delegate:".freeze
UPSTREAM_ACCESS_TOKEN "urn:masks:params:oauth:token-type:upstream_access_token".freeze
REFUSALS %w[invalid_grant insufficient_scope invalid_target unauthorized_client access_denied login_required interaction_required consent_required invalid_scope].freeze
Held Struct.new(:connection, :provider, :provider_name, :label, :subject, :secret, keyword_init: true)
Upstream Struct.new(:access_token, :expires_at, :scope, :secret, keyword_init: true) do def expired?(leeway: 60) Time.now.to_i + leeway >= expires_at.to_i end end
Attribute Access
client_id R
client_secret R
issuer R
redirect_uri R
::new(issuer:, client_id:, client_secret:, redirect_uri:)
#finish(params:, started:)
#start(provider:, prompt: nil, max_age: nil, state: SecureRandom.urlsafe_base64(24))
#token(secret, connection:)
Constant Value
Connected Struct.new(:connection, :provider, :subject, :secret, :refused, :unavailable, keyword_init: true)
Attribute Access
redirect_uri R
releases R
::new(redirect_uri: "https://app.test/connect/callback", lifetime: 3600)
#approve(started, subject: "fake-subject", connection: SecureRandom.uuid)
#deny(started, error: "access_denied", description: "the person declined")
#finish(params:, started:)
#revoke(connection, reason: "the person stopped this application using that account")
#start(provider:, prompt: nil, max_age: nil, state: SecureRandom.urlsafe_base64(24))
#token(secret, connection:)
#unavailable(connection, now: true)

Inherits Error.

Attribute Access
code R
description R
secret R
::new(code, description, secret: nil)
#signed_in_again?()

Inherits Error.

Attribute Access
secret R
::new(message, secret: nil)

Inherits StandardError.

Inherits Masks::Client::Challenge.

::new(code, description, scope: nil)
Constant Value
OPEN_TIMEOUT 5
READ_TIMEOUT 10
#default_headers()
#delete(url, headers = {})
#fetch(url, headers = {})
#get(url, headers = {})
#json(verb, url, body, headers)
#parse(response)
#post_form(url, form, headers = {})
#post_json(url, body, headers = {})
#put_json(url, body, headers = {})
#request(request)
Constant Value
PATH "/handshake".freeze
GRANT_TYPES %w[authorization_code refresh_token].freeze
AUTH_METHOD "client_secret_basic".freeze
Attribute Access
backchannel_logout_uri R
issuer R
name R
redirect_uris R
resource R
return_to R
scope R
::new(issuer, name:, resource:, redirect_uris:, return_to:, scope: Session::DEFAULT_SCOPE, backchannel_logout_uri: nil)
#complete(params, state:)
#endpoint()
#redeem(token)
#start(state: SecureRandom.urlsafe_base64(32))
#url(state:)

Inherits Masks::Client::Claims.

#active?()
#member!(*roles, organization: nil)
#nickname()
#permit!(scope)
#permits?(scope)
#token_type()

Inherits Masks::Client::Error.

Constant Value
DISCOVERY_PATH "/.well-known/openid-configuration".freeze
TTL 300
Attribute Access
url R
::new(url, ttl: TTL)
::normalize(url)
::resolve(issuer, ttl: TTL)
#avatar_styles()
#avatar_url(subject, style: nil, size: nil)
#backchannel_logout?()
#discovery()
#endpoint(name)
#jwks()
#refresh!()
#tenant()
Constant Value
EVENT "http://schemas.openid.net/event/backchannel-logout".freeze
ALGORITHMS Verifier::ALGORITHMS
LEEWAY 60
Attribute Access
claims R
::new(claims)
::verify(token, issuer:, audience:, algorithms: ALGORITHMS)
#issued_at()
#jti()
#sid()
#subject()
#validate!()
Constant Value
METHOD "S256".freeze
Attribute Access
verifier R
::generate()
::new(verifier)
#challenge()
#method()
Constant Value
SCHEME "DPoP".freeze
TYPE "dpop+jwt".freeze
ALGORITHMS %w[ES256 ES384 ES512 PS256 PS384 PS512 RS256].freeze
SECRET %w[d p q dp dq qi k].freeze
THUMBED { "EC" => %w[crv kty x y], "RSA" => %w[e kty n] }.freeze
LEEWAY 30
WINDOW 60
MEMORY WINDOW + (LEEWAY * 2)
MEMORY_LOCK Mutex.new
::digest(value)
::memory()
::new(proof, method:, url:, replay: nil)
::thumbprint(jwk)
#check!(access_token:, jkt:)

Inherits Masks::Client::Error.

::new()
#first?(key, expires_in:)
Constant Value
CLAIMS "masks.claims".freeze
ERROR "masks.error".freeze
::new(app, resource:, scope: nil, only: nil, optional: false)
#call(env)
Constant Value
REFUSED [ 401, 403 ].freeze
GONE (REFUSED + [ 404 ]).freeze
Attribute Access
access_token R
issuer R
metadata R
::create(issuer, token: nil, **attributes)
::fallback(value, default)
::held(issuer, credentials)
::new(issuer, body)
::stringify(attributes)
#authorization()
#client_id()
#client_secret()
#delete()
#known?()
#read()
#session(redirect_uri:, scope: Session::DEFAULT_SCOPE)
#update(**attributes)
#uri()
::new()
#[](url, ttl: Issuer::TTL)
#clear!()

Inherits Masks::Client::Error.

Attribute Access
code R
description R
status R
::new(code, description, status: nil)
Constant Value
PRESENTED `/\A(Bearer
METADATA_PATH "/.well-known/oauth-protected-resource".freeze
REQUIRED %w[iss sub exp].freeze
Attribute Access
issuer R
scopes R
url R
::new(issuer:, url:, scopes: [], metadata_url: nil, algorithms: Verifier::ALGORITHMS, required: REQUIRED, verifier: nil, replay: Proof.memory)
#authenticate(authorization, scope: nil, role: nil, organization: nil, proof: nil, method: nil, url: nil)
#challenge(error = nil)
#metadata()
#metadata_url()
#token(authorization)
Constant Value
DEFAULT_SCOPE %w[openid profile email].freeze
ORGANIZATION "organization".freeze
ORGANIZATION_KEY /\A[a-z0-9][a-z0-9-]*\z/
ASSERTION_TYPE "urn:ietf:params:oauth:client-assertion-type:jwt-bearer".freeze
ASSERTION_LIFETIME 60
Attribute Access
client_id R
client_secret R
issuer R
key_id R
private_key R
redirect_uri R
scope R
::new(issuer:, client_id:, redirect_uri: nil, client_secret: nil, private_key: nil, key_id: nil, scope: DEFAULT_SCOPE)
::organization_key(value)
#client_credentials(scope: nil, resource: nil)
#complete(code:, verifier:, resource: nil)
#end_session_url(post_logout_redirect_uri: nil, state: nil, id_token_hint: nil)
#exchange(subject_token, scope: nil, resource: nil, lifetime: nil, requested_token_type: nil, audience: nil, subject_token_type: Tokens::ACCESS_TOKEN, actor_token: nil, actor_token_type: Tokens::ACCESS_TOKEN)
#identity(tokens)
#introspect(token, hint: nil)
#logout_token(token)
#profile(tokens)
#refresh(refresh_token, resource: nil, scope: nil)
#revoke(token, hint: nil)
#start(resource: nil, prompt: nil, scope: nil, state: SecureRandom.urlsafe_base64(24), nonce: SecureRandom.urlsafe_base64(24), max_age: nil, organization: nil)
#userinfo(access_token)
Attribute Access
held R
::new(held = {})
#read()
#write(value)
::new(session, key)
#read()
#write(value)
Constant Value
EXCHANGE "urn:ietf:params:oauth:grant-type:token-exchange".freeze
ACCESS_TOKEN "urn:ietf:params:oauth:token-type:access_token".freeze
ID_TOKEN "urn:ietf:params:oauth:token-type:id_token".freeze
Attribute Access
access_token R
delegations R
expires_in R
id_token R
obtained_at R
refresh_token R
scope R
token_type R
::from_h(data)
::granted(body)
::new(body)
#authorization()
#expired?(leeway: 30)
#expires_at()
#scopes()
#to_h()
Constant Value
TTL 3600
LIMIT 5
Attribute Access
limit R
store R
ttl R
::new(store, ttl: TTL, limit: LIMIT)
#amend(id, **data)
#claim(id)
#clear!()
#open(**data)
#size()
Attribute Access
data R
expires_at R
id R
opened_at R
::from_h(id, held)
::new(id:, data:, opened_at:, expires_at:)
#[](key)
#live?(now = Time.now.to_f)
#to_h()

Inherits Masks::Client::Challenge.

::new(description = "a bearer token is required", code: nil)

Inherits Masks::Client::Challenge.

::new(description, code: "invalid_token", dpop: false)

Inherits Masks::Client::Error.

Inherits Masks::Client::Rejected.

Constant Value
CODE "invalid_client".freeze
::raised_by?(body)
Constant Value
ALGORITHMS %w[RS256 ES256].freeze
ACCESS_TOKEN "at+jwt".freeze
LOGOUT_TOKEN "logout+jwt".freeze
ID_TOKEN [ "jwt", nil ].freeze
Attribute Access
audience R
issuer R
::new(issuer, audience:, algorithms: ALGORITHMS)
#verify(token, required: %w[iss sub exp], typ: ID_TOKEN)