Dual-Channel: Password + Push Approval (Recommended)
Recommendation — The password column on the right of 1pass's
/session/newadopts a dual-channel pattern: a single "Verify" button sends a push to the logi app while at the same time letting the user sign in by entering their password. We recommend the same pattern to RPs that build their own login UI.
Why dual-channel
No single channel covers every failure case.
| Single channel | Failure scenario |
|---|---|
| App Universal Link only | Redirects blocked in KakaoTalk / Naver / Facebook in-app browsers |
| Push approval only (one-tap approve) | Push not received (battery saver / Doze / permission denied / token expired) |
| Password only | Painful mobile typing when there is no password manager on the phone, and no audit trail |
Keeping both paths alive in parallel means the user can:
- When the push arrives → tap [Approve] once on the phone → automatic redirect
- When the push is late or never arrives → enter the password right there → sign in normally
- When the password is forgotten → wait for the push, or use another path such as a magic link or passkey
Whichever finishes first wins. The very fact that both are alive also provides a security-alert (audit) effect — if the attempt is not the user's own, they notice it immediately via the push.
UI pattern
┌─ Sign in with email ───────────────────────┐
│ │
│ Email │
│ [you@example.com ] │
│ │
│ [📲 Verify with the logi app] │
│ A notification goes to the app. Approve │
│ in the app, or sign in with the password │
│ below. │
│ │
│ Password │
│ [•••••••• ] │
│ │
│ [Sign in] │
│ │
│ Forgot your password? │
└────────────────────────────────────────────┘Key points:
- A single email field shared by both paths — the user does not enter the same value twice
- "Verify" is a secondary button (text-xs / glass-button) — password + sign-in remains the primary path
- Status is shown inline — in a small area below the form, such as "✓ Notification sent" / "✗ Failed to send"
- Background polling — keep polling
/oauth/push/:id/statuseven while the user types the password. When approval comes in, redirect automatically
1pass internal implementation (app/views/sessions/new.html.erb)
<div class="glass-card"
data-test-id="email-fallback-column"
<% if @qr_oauth_params.present? %>
data-controller="push-approval"
data-push-approval-start-url-value="<%= oauth_push_start_path %>"
data-push-approval-status-url-template-value="<%= oauth_push_status_path(':id') %>"
data-push-approval-complete-url-template-value="<%= oauth_push_complete_path(':id') %>"
data-push-approval-oauth-params-value="<%= @qr_oauth_params.to_json %>"
<% end %>>
<%= form_with url: session_url, local: true do |form| %>
<%= form.email_field :email_address,
data: { push_approval_target: "email" } %>
<% if @qr_oauth_params.present? %>
<button type="button"
data-action="click->push-approval#start"
data-push-approval-target="submit">
📲 logi 앱으로 확인하기
</button>
<div data-push-approval-target="statusBox" hidden></div>
<div data-push-approval-target="errorBox" hidden></div>
<% end %>
<%= form.password_field :password %>
<%= form.submit "로그인" %>
<% end %>
</div>Key points:
data-controller="push-approval"mounts only when there is RP context (@qr_oauth_params)- The email input lives inside the form but is also shared with Stimulus via
data-push-approval-target="email" - "Verify" is
type="button", so no form submit happens — it only triggers Stimulus - The "Sign in" submit is the ordinary
POST /sessionas before — unrelated to Stimulus
The pattern an RP building its own login UI should follow
If an RP builds its own login form via embedded auth instead of going through the 1pass /oauth/authorize consent screen:
- An email field + a secondary "Verify (optional)" button
- On click, the browser calls
POST /oauth/push/start— with a body of{ email, client_id, redirect_uri, code_challenge, code_challenge_method, state, scope }(passing the OAuth authorize params through as-is). The response is{ request_id, expires_in }. - With the returned
request_id, the client pollsGET /oauth/push/:id/status(at 1.5s intervals) - On an
approvedresponse →window.location = /oauth/push/:id/complete(the server issues the grant and 302-bounces to the RP redirect_uri). Approval on the mobile side isPOST /api/v1/push_approvals/:id/approve(called by the logi app, unrelated to the external RP). - At the same time, keep proof-of-identity paths such as password/passkey alive so the user can finish via whichever comes first
For the full sequence diagram of the push approval flow itself, see Demo Page Walkthrough — Push Approval. The server-side endpoints are Oauth::PushApprovalsController (POST /oauth/push/start, GET /oauth/push/:id/status, GET /oauth/push/:id/complete) and the mobile-side Api::V1::PushApprovalsController (POST /api/v1/push_approvals/:id/{approve,deny}).
Security considerations
| Item | Safety guarantee |
|---|---|
| Email enumeration | /oauth/push/start returns 200 even for an unregistered email and issues a synthetic request_id. Polling stays pending forever. Enumeration is blocked. |
| Push bombing (DoS) | PushApprovalRequest has a cool-down (limits retries for the same email within a TTL window). Rate-limited at the ingress stage. |
| MITM / channel confusion | request_id is an unguessable UUID. /oauth/push/:id/complete verifies both the issued client's cookie and the OAuth state. |
| Password exposure when the user is absent | The push notification body states "A password sign-in is being attempted" and a [Block] tap is available within 30 seconds. An attempt that is not the user's own is blocked immediately. |
Anti-patterns
- ❌ "Verify" disables the password field — users who never receive the push cannot escape
- ❌ "Verify" auto-reloads after sending the push — you lose the background polling, which is disruptive to a user mid-password-entry
- ❌ "Verify" has the same position/style as the submit button — users get confused. Clearly demote it to a secondary affordance
- ❌ Two email fields — do not make the user enter the same value twice
Related
- Demo Page Walkthrough — the demo RP's actual flow + push approval sequence
- Public Clients — the self-authentication pattern for RPs without a secret
- Post-login Destination — routing rules after sign-in completes