Skip to content

fix: make it run (PPT-2536)#1

Draft
chillfox wants to merge 9 commits into
masterfrom
PPT-2536-replace-ruby-auth-with-auth-cr
Draft

fix: make it run (PPT-2536)#1
chillfox wants to merge 9 commits into
masterfrom
PPT-2536-replace-ruby-auth-with-auth-cr

Conversation

@chillfox

@chillfox chillfox commented Jul 9, 2026

Copy link
Copy Markdown

No description provided.

chillfox added 7 commits July 7, 2026 16:53
The authorization_code grant relied on Authly's default
grant_strategy.user_id (nil), so issued tokens carried a random sub.
As a result the ClaimsProvider could not resolve the user and omitted
the aud claim and the legacy u{n,e,p,r} metadata block.

Recover the user_id captured in the authorization code (matching the
legacy Ruby Doorkeeper::JWT token, whose sub is the user id) so sub,
aud, and u are populated for downstream PlaceOS services.

Adds spec/controllers/oauth_token_claims_spec.cr covering the code flow.
Authly builds the scope claim as a space-separated string, but the
legacy Ruby service emitted scope: Array(opts[:scopes]) and downstream
PlaceOS services decode it into UserJWT#scope : Array(Scope). A string
scope makes UserJWT.decode raise. Split the scope string into an array
in the ClaimsProvider so issued tokens are decodable by consumers.
Authly adds a cid (client id) claim that the legacy Ruby Doorkeeper
token never carried. Delete it in the ClaimsProvider so the emitted
claim set matches the legacy token shape. The persisted OAuthToken
record still records the client id via the token-store metadata, which
is populated independently of the JWT payload.
The legacy Ruby service mounts its Doorkeeper OAuth server endpoints
under /auth/oauth/* (token, authorize, revoke, userinfo), and that path
family is the documented public contract for server-to-server and
native app clients (auth/README.md, auth/docs/sample_auth.md). auth.cr
serves them at /auth/*. Add /auth/oauth/* route aliases on the existing
actions so clients that hardcode the Doorkeeper paths keep working after
the swap. The OIDC discovery document continues to advertise the /auth/*
paths for discovery-driven clients.
The legacy Ruby service accepts browser login forms posted as
application/x-www-form-urlencoded, but action-controller only registers
a JSON body parser by default, so POST /auth/signin returned 415 for
form posts. SigninBody already defined .from_form; wire it up by
registering a form-urlencoded body parser on the Application base
controller so form logins are accepted (202 + Set-Cookie), matching the
legacy behaviour.
The legacy Ruby logout redirects via redirect_continue(continue || "/"),
issuing a 302 to the continue target when safe, to "/" when no continue
is given, and to the authority logout_url only as the cross-domain
fallback. auth.cr issued a 303 and defaulted to the authority logout_url
even with no continue. Align the target selection and use 302 so logout
behaves as a drop-in for existing clients.
Authly's AccessToken#expires_in derives from an ACCESS_TTL constant
captured at class-load time, before configure! applies the 2-hour access
TTL, so the token response reported the authly default of 3600s while
the JWT exp claim was a full 7200s. Report the live configured TTL so
the RFC 6749 expires_in matches both the JWT exp and the legacy 2-hour
Ruby service.
@chillfox chillfox self-assigned this Jul 9, 2026
@chillfox chillfox changed the title Ppt 2536 replace ruby auth with auth cr Ppt 2536 replace ruby auth with auth.cr (PPT-2536) Jul 9, 2026
@chillfox chillfox changed the title Ppt 2536 replace ruby auth with auth.cr (PPT-2536) fix: make it run (PPT-2536) Jul 9, 2026
chillfox added 2 commits July 14, 2026 13:36
External IdP app registrations pin the exact redirect_uri the auth
service sends. The legacy Ruby service (generic_oauth#callback_url) sends
.../auth/oauth2/callback?id=<strat>, and its RewriteRedirectResponse
middleware rewrites that to the path form .../auth/oauth2/callback/<strat>
for *.b2clogin.com hosts (Azure B2C won't round-trip a query string on
redirect_uri). auth.cr was sending a bare .../callback with no id, which
every external IdP would reject as an unregistered redirect_uri, and the
strat id never round-tripped.

- callback_uri now carries ?id=<strat>, byte-identical to the legacy URL,
  used for both the authorize redirect and the token exchange.
- Add rewrite_b2clogin_redirect to convert the authorize redirect's
  encoded redirect_uri to the path form for *.b2clogin.com hosts,
  mirroring RewriteRedirectResponse. The inbound path form was already
  handled by the callback_alias route (RewriteCallbackRequest equivalent).

This makes external OAuth2 SSO a true drop-in: no IdP re-registration.
The legacy Ruby service registered SAML as the OmniAuth strategy 'adfs'
(generic_adfs). That name is baked into external IdP registrations, the
/auth/adfs kickoff+callback paths, the login-event payload, and the
UserAuthLookup id (auth-<authority>-adfs-<uid>). auth.cr used 'saml',
which would 404 the /auth/adfs callback and \u2014 worse \u2014 fail to resolve
every existing SAML-linked account (recreating users instead).

It also advertised its own computed callback as the SAML ACS URL rather
than the DB-configured assertion_consumer_service_url the IdP has
registered, and never sent the strat's name_identifier_format.

- Register the SAML factory under 'adfs' (primary) with 'saml' kept as
  an internal alias; the recorded provider name is always 'adfs'.
- Advertise strat.assertion_consumer_service_url (DB) as the ACS URL.
- Pass strat.name_identifier_format through to the AuthnRequest.

Makes SAML SSO a true drop-in: no IdP re-registration, no orphaned
UserAuthLookup rows. Adds SAMLRequest-decoding specs asserting the ACS
URL + issuer come from the DB and that /auth/adfs is served.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant