mirror of
https://github.com/maybe-finance/maybe.git
synced 2025-07-18 20:59:39 +02:00
Fix OAuth mobile app support with custom URL schemes
- Configure Doorkeeper to allow custom URL schemes (maybeapp://)
- Disable force_ssl_in_redirect_uri to support non-HTTPS schemes
- Add custom Doorkeeper views with mobile OAuth detection
- Disable Turbo for mobile OAuth flows to prevent redirect interference
- Add display parameter preservation through OAuth flow
- Create custom Doorkeeper layouts with proper styling
- Add comprehensive integration tests for mobile OAuth flows
- Ensure all OAuth pages use proper doorkeeper/application layout
This allows the mobile app to complete OAuth authorization flows
without the web app interfering with custom URL scheme redirects.
🤖 Generated with Claude Code
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
404066eaa1
commit
cba0bdf0e2
17 changed files with 513 additions and 4 deletions
6
app/views/doorkeeper/applications/_delete_form.html.erb
Normal file
6
app/views/doorkeeper/applications/_delete_form.html.erb
Normal file
|
@ -0,0 +1,6 @@
|
|||
<%- submit_btn_css ||= 'btn btn-link' %>
|
||||
<%= form_tag oauth_application_path(application), method: :delete do %>
|
||||
<%= submit_tag t('doorkeeper.applications.buttons.destroy'),
|
||||
onclick: "return confirm('#{ t('doorkeeper.applications.confirmations.destroy') }')",
|
||||
class: submit_btn_css %>
|
||||
<% end %>
|
59
app/views/doorkeeper/applications/_form.html.erb
Normal file
59
app/views/doorkeeper/applications/_form.html.erb
Normal file
|
@ -0,0 +1,59 @@
|
|||
<%= form_for application, url: doorkeeper_submit_path(application), as: :doorkeeper_application, html: { role: 'form' } do |f| %>
|
||||
<% if application.errors.any? %>
|
||||
<div class="alert alert-danger" data-alert><p><%= t('doorkeeper.applications.form.error') %></p></div>
|
||||
<% end %>
|
||||
|
||||
<div class="form-group row">
|
||||
<%= f.label :name, class: 'col-sm-2 col-form-label font-weight-bold' %>
|
||||
<div class="col-sm-10">
|
||||
<%= f.text_field :name, class: "form-control #{ 'is-invalid' if application.errors[:name].present? }", required: true %>
|
||||
<%= doorkeeper_errors_for application, :name %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<%= f.label :redirect_uri, class: 'col-sm-2 col-form-label font-weight-bold' %>
|
||||
<div class="col-sm-10">
|
||||
<%= f.text_area :redirect_uri, class: "form-control #{ 'is-invalid' if application.errors[:redirect_uri].present? }" %>
|
||||
<%= doorkeeper_errors_for application, :redirect_uri %>
|
||||
<span class="form-text text-secondary">
|
||||
<%= t('doorkeeper.applications.help.redirect_uri') %>
|
||||
</span>
|
||||
|
||||
<% if Doorkeeper.configuration.allow_blank_redirect_uri?(application) %>
|
||||
<span class="form-text text-secondary">
|
||||
<%= t('doorkeeper.applications.help.blank_redirect_uri') %>
|
||||
</span>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<%= f.label :confidential, class: 'col-sm-2 form-check-label font-weight-bold' %>
|
||||
<div class="col-sm-10">
|
||||
<%= f.check_box :confidential, class: "checkbox #{ 'is-invalid' if application.errors[:confidential].present? }" %>
|
||||
<%= doorkeeper_errors_for application, :confidential %>
|
||||
<span class="form-text text-secondary">
|
||||
<%= t('doorkeeper.applications.help.confidential') %>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<%= f.label :scopes, class: 'col-sm-2 col-form-label font-weight-bold' %>
|
||||
<div class="col-sm-10">
|
||||
<%= f.text_field :scopes, class: "form-control #{ 'has-error' if application.errors[:scopes].present? }" %>
|
||||
<%= doorkeeper_errors_for application, :scopes %>
|
||||
<span class="form-text text-secondary">
|
||||
<%= t('doorkeeper.applications.help.scopes') %>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="col-sm-offset-2 col-sm-10">
|
||||
<%= f.submit t('doorkeeper.applications.buttons.submit'), class: 'btn btn-primary' %>
|
||||
<%= link_to t('doorkeeper.applications.buttons.cancel'), oauth_applications_path, class: 'btn btn-secondary' %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
5
app/views/doorkeeper/applications/edit.html.erb
Normal file
5
app/views/doorkeeper/applications/edit.html.erb
Normal file
|
@ -0,0 +1,5 @@
|
|||
<div class="border-bottom mb-4">
|
||||
<h1><%= t('.title') %></h1>
|
||||
</div>
|
||||
|
||||
<%= render 'form', application: @application %>
|
38
app/views/doorkeeper/applications/index.html.erb
Normal file
38
app/views/doorkeeper/applications/index.html.erb
Normal file
|
@ -0,0 +1,38 @@
|
|||
<div class="border-bottom mb-4">
|
||||
<h1><%= t('.title') %></h1>
|
||||
</div>
|
||||
|
||||
<p><%= link_to t('.new'), new_oauth_application_path, class: 'btn btn-success' %></p>
|
||||
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><%= t('.name') %></th>
|
||||
<th><%= t('.callback_url') %></th>
|
||||
<th><%= t('.confidential') %></th>
|
||||
<th><%= t('.actions') %></th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% @applications.each do |application| %>
|
||||
<tr id="application_<%= application.id %>">
|
||||
<td class="align-middle">
|
||||
<%= link_to application.name, oauth_application_path(application) %>
|
||||
</td>
|
||||
<td class="align-middle">
|
||||
<%= simple_format(application.redirect_uri) %>
|
||||
</td>
|
||||
<td class="align-middle">
|
||||
<%= application.confidential? ? t('doorkeeper.applications.index.confidentiality.yes') : t('doorkeeper.applications.index.confidentiality.no') %>
|
||||
</td>
|
||||
<td class="align-middle">
|
||||
<%= link_to t('doorkeeper.applications.buttons.edit'), edit_oauth_application_path(application), class: 'btn btn-link' %>
|
||||
</td>
|
||||
<td class="align-middle">
|
||||
<%= render 'delete_form', application: application %>
|
||||
</td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
5
app/views/doorkeeper/applications/new.html.erb
Normal file
5
app/views/doorkeeper/applications/new.html.erb
Normal file
|
@ -0,0 +1,5 @@
|
|||
<div class="border-bottom mb-4">
|
||||
<h1><%= t('.title') %></h1>
|
||||
</div>
|
||||
|
||||
<%= render 'form', application: @application %>
|
63
app/views/doorkeeper/applications/show.html.erb
Normal file
63
app/views/doorkeeper/applications/show.html.erb
Normal file
|
@ -0,0 +1,63 @@
|
|||
<div class="border-bottom mb-4">
|
||||
<h1><%= t('.title', name: @application.name) %></h1>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<h4><%= t('.application_id') %>:</h4>
|
||||
<p><code class="bg-light" id="application_id"><%= @application.uid %></code></p>
|
||||
|
||||
<h4><%= t('.secret') %>:</h4>
|
||||
<p>
|
||||
<code class="bg-light" id="secret">
|
||||
<% secret = flash[:application_secret].presence || @application.plaintext_secret %>
|
||||
<% if secret.blank? && Doorkeeper.config.application_secret_hashed? %>
|
||||
<span class="bg-light font-italic text-uppercase text-muted"><%= t('.secret_hashed') %></span>
|
||||
<% else %>
|
||||
<%= secret %>
|
||||
<% end %>
|
||||
</code>
|
||||
</p>
|
||||
|
||||
<h4><%= t('.scopes') %>:</h4>
|
||||
<p>
|
||||
<code class="bg-light" id="scopes">
|
||||
<% if @application.scopes.present? %>
|
||||
<%= @application.scopes %>
|
||||
<% else %>
|
||||
<span class="bg-light font-italic text-uppercase text-muted"><%= t('.not_defined') %></span>
|
||||
<% end %>
|
||||
</code>
|
||||
</p>
|
||||
|
||||
<h4><%= t('.confidential') %>:</h4>
|
||||
<p><code class="bg-light" id="confidential"><%= @application.confidential? %></code></p>
|
||||
|
||||
<h4><%= t('.callback_urls') %>:</h4>
|
||||
|
||||
<% if @application.redirect_uri.present? %>
|
||||
<table>
|
||||
<% @application.redirect_uri.split.each do |uri| %>
|
||||
<tr>
|
||||
<td>
|
||||
<code class="bg-light"><%= uri %></code>
|
||||
</td>
|
||||
<td>
|
||||
<%= link_to t('doorkeeper.applications.buttons.authorize'), oauth_authorization_path(client_id: @application.uid, redirect_uri: uri, response_type: 'code', scope: @application.scopes), class: 'btn btn-success', target: '_blank' %>
|
||||
</td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</table>
|
||||
<% else %>
|
||||
<span class="bg-light font-italic text-uppercase text-muted"><%= t('.not_defined') %></span>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<h3><%= t('.actions') %></h3>
|
||||
|
||||
<p><%= link_to t('doorkeeper.applications.buttons.edit'), edit_oauth_application_path(@application), class: 'btn btn-primary' %></p>
|
||||
|
||||
<p><%= render 'delete_form', application: @application, submit_btn_css: 'btn btn-danger' %></p>
|
||||
</div>
|
||||
</div>
|
22
app/views/doorkeeper/authorizations/error.html.erb
Normal file
22
app/views/doorkeeper/authorizations/error.html.erb
Normal file
|
@ -0,0 +1,22 @@
|
|||
<div class="bg-container rounded-xl p-6 space-y-6">
|
||||
<div class="text-center space-y-2">
|
||||
<div class="mx-auto w-12 h-12 rounded-full bg-destructive-surface flex items-center justify-center mb-4">
|
||||
<%= icon("alert-circle", class: "w-6 h-6 text-destructive") %>
|
||||
</div>
|
||||
<h1 class="text-2xl font-medium text-primary"><%= t('doorkeeper.authorizations.error.title') %></h1>
|
||||
</div>
|
||||
|
||||
<div class="bg-surface-inset rounded-lg p-4">
|
||||
<p class="text-sm text-secondary">
|
||||
<%= (local_assigns[:error_response] ? error_response : @pre_auth.error_response).body[:error_description] %>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="text-center">
|
||||
<%= render LinkComponent.new(
|
||||
text: "Go back",
|
||||
href: "javascript:history.back()",
|
||||
variant: :secondary
|
||||
) %>
|
||||
</div>
|
||||
</div>
|
22
app/views/doorkeeper/authorizations/form_post.html.erb
Normal file
22
app/views/doorkeeper/authorizations/form_post.html.erb
Normal file
|
@ -0,0 +1,22 @@
|
|||
<div class="bg-container rounded-xl p-6 space-y-6">
|
||||
<div class="text-center space-y-2">
|
||||
<div class="mx-auto w-12 h-12 rounded-full bg-surface-inset flex items-center justify-center mb-4">
|
||||
<%= icon("loader-circle", class: "w-6 h-6 text-primary animate-spin") %>
|
||||
</div>
|
||||
<h1 class="text-2xl font-medium text-primary"><%= t('.title') %></h1>
|
||||
<p class="text-sm text-secondary">Redirecting you back to the application...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<% turbo_disabled = @pre_auth.redirect_uri&.start_with?('maybeapp://') || params[:display] == 'mobile' %>
|
||||
<%= form_tag @pre_auth.redirect_uri, method: :post, name: :redirect_form, authenticity_token: false, data: { turbo: !turbo_disabled } do %>
|
||||
<% auth.body.compact.each do |key, value| %>
|
||||
<%= hidden_field_tag key, value %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<script>
|
||||
window.onload = function () {
|
||||
document.forms['redirect_form'].submit();
|
||||
};
|
||||
</script>
|
76
app/views/doorkeeper/authorizations/new.html.erb
Normal file
76
app/views/doorkeeper/authorizations/new.html.erb
Normal file
|
@ -0,0 +1,76 @@
|
|||
<% if params[:redirect_uri]&.start_with?('maybeapp://') || params[:display] == 'mobile' %>
|
||||
<meta name="turbo-visit-control" content="reload">
|
||||
<% end %>
|
||||
|
||||
<div class="bg-container rounded-xl p-6 space-y-6">
|
||||
<div class="space-y-2 text-center">
|
||||
<p class="text-sm text-secondary">
|
||||
<%= raw t('.prompt', client_name: content_tag(:span, @pre_auth.client.name, class: 'font-medium text-primary')) %>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<% if @pre_auth.scopes.count > 0 %>
|
||||
<div class="bg-surface-inset rounded-lg p-4 space-y-3">
|
||||
<p class="text-sm font-medium text-primary"><%= t('.able_to') %>:</p>
|
||||
<ul class="space-y-2">
|
||||
<% @pre_auth.scopes.each do |scope| %>
|
||||
<li class="flex items-start gap-2 text-sm text-secondary">
|
||||
<%= icon("check", class: "w-4 h-4 mt-0.5 text-success") %>
|
||||
<span><%= t scope, scope: [:doorkeeper, :scopes] %></span>
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div class="space-y-3">
|
||||
<% turbo_disabled = params[:redirect_uri]&.start_with?('maybeapp://') || params[:display] == 'mobile' %>
|
||||
<%= form_tag oauth_authorization_path, method: :post, class: "w-full", data: { turbo: !turbo_disabled } do %>
|
||||
<%= hidden_field_tag :client_id, @pre_auth.client.uid, id: nil %>
|
||||
<%= hidden_field_tag :redirect_uri, @pre_auth.redirect_uri, id: nil %>
|
||||
<%= hidden_field_tag :state, @pre_auth.state, id: nil %>
|
||||
<%= hidden_field_tag :response_type, @pre_auth.response_type, id: nil %>
|
||||
<%= hidden_field_tag :response_mode, @pre_auth.response_mode, id: nil %>
|
||||
<%= hidden_field_tag :scope, @pre_auth.scope, id: nil %>
|
||||
<%= hidden_field_tag :code_challenge, @pre_auth.code_challenge, id: nil %>
|
||||
<%= hidden_field_tag :code_challenge_method, @pre_auth.code_challenge_method, id: nil %>
|
||||
<% if params[:display].present? %>
|
||||
<%= hidden_field_tag :display, params[:display], id: nil %>
|
||||
<% end %>
|
||||
<%= render ButtonComponent.new(
|
||||
text: t('doorkeeper.authorizations.buttons.authorize'),
|
||||
variant: :primary,
|
||||
size: :lg,
|
||||
full_width: true,
|
||||
href: oauth_authorization_path,
|
||||
data: { disable_with: "Authorizing..." }
|
||||
) %>
|
||||
<% end %>
|
||||
|
||||
<%= form_tag oauth_authorization_path, method: :delete, class: "w-full", data: { turbo: !turbo_disabled } do %>
|
||||
<%= hidden_field_tag :client_id, @pre_auth.client.uid, id: nil %>
|
||||
<%= hidden_field_tag :redirect_uri, @pre_auth.redirect_uri, id: nil %>
|
||||
<%= hidden_field_tag :state, @pre_auth.state, id: nil %>
|
||||
<%= hidden_field_tag :response_type, @pre_auth.response_type, id: nil %>
|
||||
<%= hidden_field_tag :response_mode, @pre_auth.response_mode, id: nil %>
|
||||
<%= hidden_field_tag :scope, @pre_auth.scope, id: nil %>
|
||||
<%= hidden_field_tag :code_challenge, @pre_auth.code_challenge, id: nil %>
|
||||
<%= hidden_field_tag :code_challenge_method, @pre_auth.code_challenge_method, id: nil %>
|
||||
<% if params[:display].present? %>
|
||||
<%= hidden_field_tag :display, params[:display], id: nil %>
|
||||
<% end %>
|
||||
<%= render ButtonComponent.new(
|
||||
text: t('doorkeeper.authorizations.buttons.deny'),
|
||||
variant: :outline,
|
||||
size: :lg,
|
||||
full_width: true,
|
||||
href: oauth_authorization_path,
|
||||
data: { disable_with: "Denying..." }
|
||||
) %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<p class="text-xs text-tertiary text-center">
|
||||
By authorizing, you allow this app to access your Maybe data according to the permissions above.
|
||||
</p>
|
||||
</div>
|
17
app/views/doorkeeper/authorizations/show.html.erb
Normal file
17
app/views/doorkeeper/authorizations/show.html.erb
Normal file
|
@ -0,0 +1,17 @@
|
|||
<div class="bg-container rounded-xl p-6 space-y-6">
|
||||
<div class="text-center space-y-2">
|
||||
<div class="mx-auto w-12 h-12 rounded-full bg-success-surface flex items-center justify-center mb-4">
|
||||
<%= icon("check", class: "w-6 h-6 text-success") %>
|
||||
</div>
|
||||
<h1 class="text-2xl font-medium text-primary"><%= t('.title') %></h1>
|
||||
</div>
|
||||
|
||||
<div class="bg-surface-inset rounded-lg p-4">
|
||||
<p class="text-xs text-secondary mb-2">Authorization Code:</p>
|
||||
<code id="authorization_code" class="block text-sm font-mono text-primary break-all"><%= params[:code] %></code>
|
||||
</div>
|
||||
|
||||
<p class="text-sm text-secondary text-center">
|
||||
Copy this code and paste it into the application.
|
||||
</p>
|
||||
</div>
|
|
@ -0,0 +1,4 @@
|
|||
<%- submit_btn_css ||= 'btn btn-link' %>
|
||||
<%= form_tag oauth_authorized_application_path(application), method: :delete do %>
|
||||
<%= submit_tag t('doorkeeper.authorized_applications.buttons.revoke'), onclick: "return confirm('#{ t('doorkeeper.authorized_applications.confirmations.revoke') }')", class: submit_btn_css %>
|
||||
<% end %>
|
24
app/views/doorkeeper/authorized_applications/index.html.erb
Normal file
24
app/views/doorkeeper/authorized_applications/index.html.erb
Normal file
|
@ -0,0 +1,24 @@
|
|||
<header class="page-header">
|
||||
<h1><%= t('doorkeeper.authorized_applications.index.title') %></h1>
|
||||
</header>
|
||||
|
||||
<main role="main">
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><%= t('doorkeeper.authorized_applications.index.application') %></th>
|
||||
<th><%= t('doorkeeper.authorized_applications.index.created_at') %></th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% @applications.each do |application| %>
|
||||
<tr>
|
||||
<td><%= application.name %></td>
|
||||
<td><%= application.created_at.strftime(t('doorkeeper.authorized_applications.index.date_format')) %></td>
|
||||
<td><%= render 'delete_form', application: application %></td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
</main>
|
39
app/views/layouts/doorkeeper/admin.html.erb
Normal file
39
app/views/layouts/doorkeeper/admin.html.erb
Normal file
|
@ -0,0 +1,39 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title><%= t('doorkeeper.layouts.admin.title') %></title>
|
||||
<%= stylesheet_link_tag "doorkeeper/admin/application" %>
|
||||
<%= csrf_meta_tags %>
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-dark mb-5">
|
||||
<%= link_to t('doorkeeper.layouts.admin.nav.oauth2_provider'), oauth_applications_path, class: 'navbar-brand' %>
|
||||
|
||||
<div class="collapse navbar-collapse">
|
||||
<ul class="navbar-nav mr-auto">
|
||||
<li class="nav-item <%= 'active' if request.path == oauth_applications_path %>">
|
||||
<%= link_to t('doorkeeper.layouts.admin.nav.applications'), oauth_applications_path, class: 'nav-link' %>
|
||||
</li>
|
||||
<% if respond_to?(:root_path) %>
|
||||
<li class="nav-item">
|
||||
<%= link_to t('doorkeeper.layouts.admin.nav.home'), root_path, class: 'nav-link' %>
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="doorkeeper-admin container">
|
||||
<%- if flash[:notice].present? %>
|
||||
<div class="alert alert-info">
|
||||
<%= flash[:notice] %>
|
||||
</div>
|
||||
<% end -%>
|
||||
|
||||
<%= yield %>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
48
app/views/layouts/doorkeeper/application.html.erb
Normal file
48
app/views/layouts/doorkeeper/application.html.erb
Normal file
|
@ -0,0 +1,48 @@
|
|||
<!DOCTYPE html>
|
||||
<% theme = Current.user&.theme || "system" %>
|
||||
<html
|
||||
lang="en"
|
||||
data-theme="<%= theme %>"
|
||||
data-controller="theme"
|
||||
data-theme-user-preference-value="<%= Current.user&.theme || "system" %>"
|
||||
class="h-full text-primary overflow-hidden lg:overflow-auto font-sans">
|
||||
<head>
|
||||
<%= render "layouts/shared/head" %>
|
||||
</head>
|
||||
|
||||
<body class="h-full overflow-hidden lg:overflow-auto antialiased">
|
||||
<div class="flex flex-col h-full">
|
||||
<div class="flex flex-col h-full px-6 py-12 bg-surface">
|
||||
<div class="grow flex flex-col justify-center">
|
||||
<div class="sm:mx-auto sm:w-full sm:max-w-md">
|
||||
<div class="flex justify-center mt-2 md:mb-6">
|
||||
<%= image_tag "logo-color.png", class: "w-16 mb-6" %>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<h2 class="text-3xl font-medium text-primary text-center">
|
||||
Maybe Authorization
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-5 md:mt-8 sm:mx-auto sm:w-full sm:max-w-lg">
|
||||
<%- if flash[:notice].present? %>
|
||||
<div class="mb-4 p-3 rounded-lg bg-surface-inset text-sm text-secondary">
|
||||
<%= flash[:notice] %>
|
||||
</div>
|
||||
<% end -%>
|
||||
<%- if flash[:alert].present? %>
|
||||
<div class="mb-4 p-3 rounded-lg bg-destructive-surface text-sm text-destructive">
|
||||
<%= flash[:alert] %>
|
||||
</div>
|
||||
<% end -%>
|
||||
|
||||
<%= yield %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%= render "layouts/shared/footer" %>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
|
@ -303,9 +303,8 @@ Doorkeeper.configure do
|
|||
# #call can be used in order to allow conditional checks (to allow non-SSL
|
||||
# redirects to localhost for example).
|
||||
#
|
||||
# force_ssl_in_redirect_uri !Rails.env.development?
|
||||
#
|
||||
# force_ssl_in_redirect_uri { |uri| uri.host != 'localhost' }
|
||||
# Allow custom URL schemes for mobile apps
|
||||
force_ssl_in_redirect_uri false
|
||||
|
||||
# Specify what redirect URI's you want to block during Application creation.
|
||||
# Any redirect URI is allowed by default.
|
||||
|
@ -313,7 +312,8 @@ Doorkeeper.configure do
|
|||
# You can use this option in order to forbid URI's with 'javascript' scheme
|
||||
# for example.
|
||||
#
|
||||
# forbid_redirect_uri { |uri| uri.scheme.to_s.downcase == 'javascript' }
|
||||
# Block javascript URIs but allow custom schemes
|
||||
forbid_redirect_uri { |uri| uri.scheme.to_s.downcase == 'javascript' }
|
||||
|
||||
# Allows to set blank redirect URIs for Applications in case Doorkeeper configured
|
||||
# to use URI-less OAuth grant flows like Client Credentials or Resource Owner
|
||||
|
|
6
config/initializers/doorkeeper_layout.rb
Normal file
6
config/initializers/doorkeeper_layout.rb
Normal file
|
@ -0,0 +1,6 @@
|
|||
# Ensure Doorkeeper controllers use the correct layout
|
||||
Rails.application.config.to_prepare do
|
||||
Doorkeeper::AuthorizationsController.layout "doorkeeper/application"
|
||||
Doorkeeper::AuthorizedApplicationsController.layout "doorkeeper/application"
|
||||
Doorkeeper::ApplicationsController.layout "doorkeeper/application"
|
||||
end
|
75
test/integration/oauth_mobile_test.rb
Normal file
75
test/integration/oauth_mobile_test.rb
Normal file
|
@ -0,0 +1,75 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require "test_helper"
|
||||
|
||||
class OauthMobileTest < ActionDispatch::IntegrationTest
|
||||
setup do
|
||||
@user = users(:empty)
|
||||
sign_in(@user)
|
||||
|
||||
@oauth_app = Doorkeeper::Application.create!(
|
||||
name: "Maybe Mobile App",
|
||||
redirect_uri: "maybeapp://oauth/callback",
|
||||
scopes: "read"
|
||||
)
|
||||
end
|
||||
|
||||
test "mobile oauth authorization with custom scheme redirect" do
|
||||
get "/oauth/authorize", params: {
|
||||
client_id: @oauth_app.uid,
|
||||
redirect_uri: @oauth_app.redirect_uri,
|
||||
response_type: "code",
|
||||
scope: "read",
|
||||
display: "mobile"
|
||||
}
|
||||
|
||||
assert_response :success
|
||||
|
||||
# Check that Turbo is disabled in the form
|
||||
assert_match(/data-turbo="false"/, response.body)
|
||||
assert_match(/maybeapp:\/\/oauth\/callback/, response.body)
|
||||
end
|
||||
|
||||
test "mobile oauth detects custom scheme in redirect_uri" do
|
||||
get "/oauth/authorize", params: {
|
||||
client_id: @oauth_app.uid,
|
||||
redirect_uri: "maybeapp://oauth/callback",
|
||||
response_type: "code",
|
||||
scope: "read"
|
||||
}
|
||||
|
||||
assert_response :success
|
||||
|
||||
# Should detect mobile flow from redirect_uri
|
||||
assert_match(/data-turbo="false"/, response.body)
|
||||
end
|
||||
|
||||
test "mobile oauth authorization flow completes successfully" do
|
||||
post "/oauth/authorize", params: {
|
||||
client_id: @oauth_app.uid,
|
||||
redirect_uri: @oauth_app.redirect_uri,
|
||||
response_type: "code",
|
||||
scope: "read",
|
||||
display: "mobile"
|
||||
}
|
||||
|
||||
# Should redirect to the custom scheme
|
||||
assert_response :redirect
|
||||
assert response.location.start_with?("maybeapp://oauth/callback")
|
||||
end
|
||||
|
||||
test "mobile oauth preserves display parameter through forms" do
|
||||
get "/oauth/authorize", params: {
|
||||
client_id: @oauth_app.uid,
|
||||
redirect_uri: @oauth_app.redirect_uri,
|
||||
response_type: "code",
|
||||
scope: "read",
|
||||
display: "mobile"
|
||||
}
|
||||
|
||||
assert_response :success
|
||||
|
||||
# Check that display parameter is preserved in hidden fields
|
||||
assert_match(/<input[^>]*name="display"[^>]*value="mobile"/, response.body)
|
||||
end
|
||||
end
|
Loading…
Add table
Add a link
Reference in a new issue