Universal Links 통합 가이드 (RP 측)
1pass 로 로그인 버튼을 누른 사용자에게 마찰 없이 네이티브 앱이 자동으로 열리는 경험을 만드는 패턴 정리. 1pass IdP 와 OAuth 연동하는 모든 RP (Relying Party) 가 적용 대상.
이 페이지는 RP 측 통합 패턴이고, 1pass 앱 자체의 entitlement 설정은
RELEASE_CHECKLIST.md, 트러블슈팅은 Troubleshooting 가이드 의 시나리오 4·7 참고.
핵심 제약
Apple Universal Links 는 Safari / WKWebView 안에서 클릭한 링크만 트리거된다. Apple 공식:
"With universal links, users open your app when they click links to your website within Safari and WKWebView."
— Allowing Apps and Websites to Link to Your Content
즉:
| 환경 | Universal Link 트리거 |
|---|---|
| iOS / macOS Safari | ✅ |
| iOS / macOS WKWebView (in-app browser) | ✅ |
| iOS / macOS Chrome / Firefox / Brave / Edge | ❌ |
| Safari 시크릿 모드 | ⚠️ 일관되지 않음 |
| Windows / Android / Linux | ❌ (Android 는 별도 — App Links 사용) |
| 주소창에 URL 직접 paste | ❌ (Apple 의도된 동작) |
| 같은 도메인 내 클릭 | ❌ (Apple 의도된 동작) |
따라서 RP 입장에서 "그냥 <a href>만 박아두면 알아서 앱 열림" 은 Safari 사용자에게만 사실. 비-Safari Apple OS 사용자에게는 추가 처리가 필요하다.
권장 3-tier 패턴
Tier 1 — 직접 <a href> (필수)
서버에서 OAuth authorize URL 을 미리 생성해 페이지에 직접 박는다. server-side 302 redirect 금지 (시나리오 4 참고).
<a
href="https://api.1pass.dev/oauth/authorize?response_type=code&client_id=...&..."
data-turbo="false"
rel="external"
>
1pass 로 로그인
</a>data-turbo="false" (또는 프레임워크별 등가물) 로 SPA 라우터 가로채기를 막는다. rel="external" 은 외부 도메인임을 명시.
Safari 사용자: 클릭 → OS 가 AASA 매칭 → 앱 열림. 끝.
Tier 2 — non-Safari Apple OS 핸드오프 (권장)
비-Safari 사용자가 클릭하면 x-safari-https:// URL scheme 으로 Safari 핸드오프를 시도한다.
<a
href="https://api.1pass.dev/oauth/authorize?..."
onclick="handleOnePassClick(event, this.href)"
data-turbo="false"
rel="external"
>
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()
// Safari 가 자기 자신에게 등록한 custom URL scheme.
// OS 가 인식하면 Safari 가 열리고 → AASA 매칭 → 앱 열림.
window.location.href = url.replace(/^https:/, 'x-safari-https:')
}
// Safari 또는 비-Apple OS → 기본 navigation
}
</script>왜 x-safari-https:// 인가
iOS / macOS Safari 가 자기 자신에게 등록한 historical URL scheme. Apple 공식 문서화는 없지만 iOS 14+ 부터 안정 동작. Telegram, Slack, Discord 등 대부분의 메신저가 "Safari 에서 열기" 기능에 사용한다.
비-Apple OS 또는 등록된 Safari 가 없으면 그냥 실패하므로 안전 (페이지가 "이 링크를 열 수 없습니다" 같은 가벼운 에러만 표시).
Tier 3 — 1pass /session/new 안내 banner (자동)
Tier 2 가 실패하거나 사용자가 그냥 진행한 경우, 1pass 측 로그인 페이지에서 자동으로 안내 banner 가 표시된다. 이건 RP 가 추가 작업할 필요 없음 (1pass 가 처리).
📱 1pass 앱이 설치되어 있다면 Safari 에서 열어보세요
Apple Universal Links 는 Safari 에서만 동작합니다 …조건: return_to 가 /oauth/authorize 로 시작 + UA 가 Apple OS + UA 가 non-Safari → 자동 표시.
구현 예시
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="..."
>
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">
1pass 로 로그인
</a>
)
}서버 측 (page.tsx) 에서 authorizeUrl 을 생성해 prop 으로 전달. PKCE verifier 는 cookie 또는 server-side session 에 저장.
RP 측 검증 체크리스트
새 RP 통합 후 출시 전 이걸 다 통과하는지 확인:
- [ ] Safari (iOS / macOS) 에서 1pass 버튼 → 앱이 native consent 화면으로 열림
- [ ] Mac Chrome 에서 1pass 버튼 → Safari 로 핸드오프 → 앱 열림 (Tier 2)
- [ ] iOS Chrome (CriOS) 에서 1pass 버튼 → Safari 로 핸드오프 → 앱 열림 (Tier 2)
- [ ] 앱 미설치 + Safari → 1pass 웹 로그인 페이지로 진행 (정상)
- [ ] Windows / Android Chrome → 1pass 웹 로그인 페이지로 진행 (정상, Universal Link 무관)
- [ ] 시크릿 모드 / Safari 비활성화 환경 → 1pass
/session/new의 안내 banner 노출 (Tier 3 fallback)
흔한 실수
❌ Server-side 302 redirect 로 OAuth flow 시작
RP 가 <a href="/auth/1pass"> 같은 자기 도메인 링크를 두고 → server 가 redirect_to "https://api.1pass.dev/oauth/authorize?..." 로 302 응답.
문제: macOS Universal Links 는 user-gesture 가 직접 향하는 도메인만 매칭. redirect chain 은 user-gesture 를 끊는다. iOS 14+ 는 redirect 후에도 매칭하지만 macOS 는 더 엄격.
해결: 페이지 렌더 시점에 server 에서 OAuth URL 을 미리 생성해 <a href> 에 박아둔다. PKCE verifier 는 그대로 server session 에 저장 (보안 무영향).
❌ 주소창 paste 로 검증
https://api.1pass.dev/oauth/authorize?... 를 주소창에 paste → enter 는 Apple 의도된 동작으로 Universal Link 무시. 검증할 때는 반드시 다른 도메인의 페이지에서 <a> 클릭 으로 테스트.
❌ Chrome 에서만 테스트하고 끝
Apple Universal Links 는 Safari 에서만 트리거되므로 Chrome 검증은 무의미. 반드시 Safari 정상 모드에서 검증.
❌ x-safari-https:// 만으로 충분하다고 가정
이 scheme 은 비공식 + Apple 이 언제든 차단 가능. Tier 2 가 실패하는 경우를 가정하고 Tier 3 (1pass 안내 banner) 가 항상 동작하도록 둔다. 이게 안전망.
관련 문서
- Troubleshooting — 시나리오 4·7 — 1pass 버튼 클릭해도 앱이 안 열릴 때
- OAuth Authorization Code Flow — RP 측 OAuth 표준 flow
- PKCE (RFC 7636) — code verifier / challenge
- Apple TN3155 — Debugging Universal Links
- Apple — Allowing Apps and Websites to Link to Your Content