Universal Links Integration Guide (RP Side)
Patterns for making an RP that integrates OAuth with the 1pass IdP hand off seamlessly to the native app when the user taps "Sign in with 1pass."
The core constraint
Apple Universal Links fire only for links clicked inside Safari / WKWebView.
| Environment | Universal Link triggers |
|---|---|
| iOS Safari | ✅ Opens the app immediately on click |
| macOS Safari | ⚠️ After the page loads, an "Open in [app]" banner appears at the top → the user clicks once more |
| iOS / macOS WKWebView | ✅ |
| iOS / macOS Chrome / Firefox / Edge / Brave | ❌ (x-safari-https:// fallback) |
| Safari private mode | ⚠️ Inconsistent |
| Windows / Android / Linux | ❌ (Android uses App Links separately) |
| Pasting into the address bar | ❌ (Apple-intended behavior) |
| same-domain click (api.1pass.dev → api.1pass.dev) | ⚠️ iOS ignores it entirely (TN3155); macOS shows a weak banner — work around it with the open.1pass.dev bouncer |
The open.1pass.dev Universal Link bouncer
iOS ignores the same-host click from api.1pass.dev/session/new → api.1pass.dev/oauth/authorize. A separate host, open.1pass.dev/auth?..., works around it with an AASA claim. No extra work is needed on the RP side — just embed the /oauth/authorize URL, and the server emits it for you.
Recommended 3-tier pattern
Tier 1 — Direct <a href> (required)
Generate the OAuth authorize URL on the server ahead of time and embed it directly in the page. No server-side 302 redirect.
<a
href="https://api.1pass.dev/oauth/authorize?response_type=code&client_id=...&..."
data-turbo="false"
rel="external"
>
Sign in with 1pass
</a>Use data-turbo="false" (or your framework's equivalent) to prevent the SPA router from intercepting the click.
Tier 2 — Non-Safari Apple OS handoff (recommended)
<a
href="https://api.1pass.dev/oauth/authorize?..."
onclick="handleOnePassClick(event, this.href)"
data-turbo="false"
rel="external"
>
Sign in with 1pass
</a>
<script>
function handleOnePassClick(e, url) {
if (typeof navigator === 'undefined') return // SSR guard
const ua = navigator.userAgent
const isAppleOS = /iPhone|iPad|iPod|Macintosh|Mac OS X/.test(ua)
const isNonSafari = /CriOS|FxiOS|EdgiOS|Chrome|Firefox|Edg|OPR\//.test(ua)
if (isAppleOS && isNonSafari) {
e.preventDefault()
window.location.href = url.replace(/^https:/, 'x-safari-https:')
}
}
</script>x-safari-https:// is an unofficial scheme that iOS / macOS Safari registers for itself (stable since iOS 14+). On non-Apple OSes it simply fails, so it's safe.
Tier 3 — 1pass /session/new guidance banner (automatic)
If Tier 2 fails, 1pass automatically shows a guidance banner. No extra work is needed on the RP side.
Condition: return_to starts with /oauth/authorize + the UA is an Apple OS + non-Safari.
Implementation examples
Rails (Inertia.js + Svelte 5)
# app/controllers/web/sessions_controller.rb
def new
state = SecureRandom.urlsafe_base64(32)
verifier, challenge = OnePassSsoService.generate_pkce
session[:one_pass_state] = state
session[:one_pass_code_verifier] = verifier
session[:one_pass_initiated_at] = Time.current.to_i
render inertia: "Auth/Login", props: {
one_pass_authorize_url: OnePassSsoService.authorize_url(
redirect_uri: auth_1pass_callback_url,
state: state,
code_challenge: challenge
)
}
end<!-- Login.svelte -->
<script lang="ts">
let { one_pass_authorize_url } = $props()
function handleOnePassClick(e: MouseEvent, url: string) {
if (typeof navigator === 'undefined') return
const ua = navigator.userAgent
const isAppleOS = /iPhone|iPad|iPod|Macintosh|Mac OS X/.test(ua)
const isNonSafari = /CriOS|FxiOS|EdgiOS|Chrome|Firefox|Edg|OPR\//.test(ua)
if (isAppleOS && isNonSafari) {
e.preventDefault()
window.location.href = url.replace(/^https:/, 'x-safari-https:')
}
}
</script>
<a
href={one_pass_authorize_url}
onclick={(e) => handleOnePassClick(e, one_pass_authorize_url)}
data-turbo="false"
rel="external"
class="..."
>
Sign in with 1pass
</a>Next.js (App Router)
'use client'
export function OnePassButton({ authorizeUrl }: { authorizeUrl: string }) {
const handleClick = (e: React.MouseEvent<HTMLAnchorElement>) => {
const ua = navigator.userAgent
const isAppleOS = /iPhone|iPad|iPod|Macintosh|Mac OS X/.test(ua)
const isNonSafari = /CriOS|FxiOS|EdgiOS|Chrome|Firefox|Edg|OPR\//.test(ua)
if (isAppleOS && isNonSafari) {
e.preventDefault()
window.location.href = authorizeUrl.replace(/^https:/, 'x-safari-https:')
}
}
return (
<a href={authorizeUrl} onClick={handleClick} rel="external">
Sign in with 1pass
</a>
)
}Generate authorizeUrl in the server page.tsx, and store the PKCE verifier in a cookie / server session.
RP verification checklist
- [ ] Safari (iOS / macOS) → the app opens to the native consent screen
- [ ] Mac Chrome → Safari handoff → the app opens (Tier 2)
- [ ] iOS Chrome (CriOS) → Safari handoff → the app opens (Tier 2)
- [ ] App not installed + Safari → the 1pass web login page (expected)
- [ ] Windows / Android Chrome → the 1pass web login page (expected)
- [ ] Private mode / Safari disabled → the Tier 3 guidance banner
Diagnostics: on an affected device, visit https://api.1pass.dev/diagnose to check the UA, in-app browser, AASA path claim, and a test deep-link.
Common mistakes
❌ Starting the OAuth flow with a server-side 302 redirect
<a href="/auth/1pass"> → the server does a redirect_to "https://api.1pass.dev/oauth/authorize?..." 302. macOS Universal Links match only the domain the user gesture targets directly — a redirect chain breaks the user gesture.
Fix: generate the OAuth URL on the server at page-render time and embed it in the <a href>. Store the PKCE verifier in the server session as before.
❌ Verifying by pasting into the address bar
By design, Apple ignores Universal Links in this case. Always test by clicking an <a> on a different-domain page.
❌ Testing only in Chrome
It only triggers in Safari. Verify in normal Safari mode.
❌ Trusting x-safari-https:// alone
It's unofficial and Apple may block it. The Tier 3 (1pass guidance banner) safety net is essential.
❌ The applinks domain overlapping the OAuth callback host
Symptom: the RP declares applinks:api.1pass.dev in its entitlement → the SDK consumes an unrelated universal link, producing missingCode / invalidCallback errors.
Fix:
- Upgrade to LogiAuth Swift 0.1.2+ — the SDK validates the redirect_uri scheme/host/path during a pending handoff.
- Separate the RP's
applinks:claim domain from the OAuth callback host (api.1pass.dev) — claim only the RP's own domain, and use a custom URL scheme for the callback (com.example.app://oauth/callback). - Check for an AASA path-claim conflict with
/diagnose.
❌ A Universal Link pointing directly at /oauth/authorize
For a user without the app installed, the browser opens the authorize URL as-is → if client_id / redirect_uri / code_challenge aren't prepared ahead of time, you get invalid_request.
Fix: point the Universal Link at a trampoline route (for example, /auth/1pass/start).
- App installed → the app opens via AASA matching, ignoring the trampoline's HTTP response.
- App not installed → the trampoline serves HTML with "app download guidance" or a PKCE-ready web OAuth fallback.
When the macOS Safari "Open in [App]" banner doesn't appear
- The Mac app is properly installed in
/Applications/and has been launched at least once to pass the Developer ID first-launch gate. - Don't have two copies of the same app (TestFlight + Xcode debug) installed at once — Apple enforces "one copy per Mac."
- Stale AASA cache → reinstall the app.
- Verify that
curl https://api.1pass.dev/.well-known/apple-app-site-associationincludes theMAC_APP_ID(74PTNNLD4P.com.dcodelabs.logi.mac).