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

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)
Constant Value
GRANT_TYPE "urn:ietf:params:oauth:grant-type:device_code".freeze
INTERVAL 5
REQUIRED %w[device_code user_code verification_uri].freeze
Attribute Access
device_code R
expires_in R
interval R
obtained_at R
user_code R
verification_uri R
verification_uri_complete R
::issued(body)
::new(body)
#expired?()
#expires_at()
#inspect()
Constant Value
LOOPBACK "127.0.0.1".freeze
DROPPED %w[host accept-encoding connection content-length].freeze
Attribute Access
app R
::new(app, &serves)
#deliver(request, limit: nil)
#inspect()
#serves?(uri)
Attribute Access
expires_in R
initial_access_token R
obtained_at R
scope R
::issued(body)
::new(body)
#expires_at()
#scopes()

Inherits StandardError.

Inherits Masks::Client::Challenge.

::new(code, description, scope: nil)

Inherits Masks::Client::Error.

Constant Value
OPEN_TIMEOUT 5
READ_TIMEOUT 10
READ_LIMIT 1 << 20
FETCH_LIMIT 8 << 20
#checked(uri)
#decoded(response)
#default_headers()
#delete(url, headers = {})
#delete_form(url, form, headers = {})
#deliver(request, limit: READ_LIMIT)
#encoded(verb, url, form, headers)
#fetch(url, headers = {})
#form_request(verb, url, form, headers)
#get(url, headers = {})
#json(verb, url, body, headers)
#limited(http, request, limit)
#loopback?(host)
#origin(response)
#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
issuer R
name R
post_logout_redirect_uri 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, post_logout_redirect_uri: nil)
#backchannel_logout_uri()
#complete(params, state:)
#endpoint()
#redeem(token)
#start(state: SecureRandom.urlsafe_base64(32))
#url(state:)

Inherits Masks::Client::Error.

Inherits Masks::Client::Claims.

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

Inherits Masks::Client::Error.

One issuer’s discovery document and keys, fetched once and kept for an hour. While masks cannot be reached, the last good copies are served, a token signed with a key never seen raises Unreachable, and refetches back off from 5 seconds to once a minute.

Constant Value
DISCOVERY_PATH "/.well-known/openid-configuration".freeze
LIFETIME 3600
FIRST_BACKOFF 5
LONGEST_BACKOFF 60
TTL LIFETIME
Attribute Access
nonces R
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()
#keys!()
#refresh!()
#tenant()
Constant Value
CURVE "prime256v1".freeze
ALGORITHM "ES256".freeze
PUBLIC %w[kty crv x y].freeze
Attribute Access
kid R
private_key R
thumbprint R
::from(value)
::generate()
::htu(url)
::imported(data)
::new(private_key)
::parse(data)
#inspect()
#jwks()
#proof(method, url, access_token: nil, nonce: nil)
#public_jwk()
#sign(claims, header = {})
#to_h()
#to_json(*)
#to_pem()
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)
#expires_at()
#issued_at()
#jti()
#sid()
#subject()
#validate!()
Constant Value
HEADER "DPoP-Nonce".freeze
::new()
::origin(url)
#[](url)
#learn(url, response)
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)
::endpoint(issuer, token)
::held(issuer, credentials)
::keyset(value)
::new(issuer, body)
::stringify(attributes)
#authorization()
#client_id()
#client_secret()
#delete()
#known?()
#read()
#session(redirect_uri: nil, scope: Session::DEFAULT_SCOPE, **options)
#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)

Inherits Masks::Client::Error.

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
NO_NONCE "use_dpop_nonce".freeze
Attribute Access
client_id R
client_secret R
dpop 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, dpop: nil, scope: DEFAULT_SCOPE)
::organization_key(value)
#await_device(device, pause: Device::INTERVAL)
#client_credentials(scope: nil, resource: nil)
#complete(code:, verifier:, resource: nil)
#device_authorization(scope: nil, resource: nil)
#end_session_url(post_logout_redirect_uri: nil, state: nil, id_token_hint: nil)
#enroll(scope:, name: nil, expires_in: 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, max_age: nil)

The person the tokens belong to: the ID token’s claims merged over userinfo. With max_age, the seconds the app asked for when it started the sign-in, an ID token whose auth_time is missing or older than that, plus 30 seconds of leeway, raises InvalidToken.

#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)
#transport(*origins, tokens: nil, scope: nil, resource: nil, &keep)
#unenroll(machine_id)
#userinfo(tokens)
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()
#bound?()
#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()
Constant Value
VERBS { "GET" => Net::HTTP::Get, "HEAD" => Net::HTTP::Head, "POST" => Net::HTTP::Post, "PUT" => Net::HTTP::Put, "PATCH" => Net::HTTP::Patch, "DELETE" => Net::HTTP::Delete }.freeze
Attribute Access
key R
origins R
::new(*origins, tokens: nil, key: nil, &renew)
#delete(url, headers: {})
#get(url, headers: {})
#head(url, headers: {})
#inspect()
#patch(url, body: nil, headers: {})
#post(url, body: nil, headers: {})
#put(url, body: nil, headers: {})
#request(method, url, body: nil, headers: {}, limit: nil)
#tokens()

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
AUTH_TIME_LEEWAY 30
Attribute Access
audience R
issuer R
::new(issuer, audience:, algorithms: ALGORITHMS)
::recent!(claims, max_age)
#verify(token, required: %w[iss sub exp], typ: ID_TOKEN)