Skip to content

Commit 22ce5e3

Browse files
author
Nils Henning
committed
[TASK] finish essential authentication guide, update devise guide
1 parent a60112b commit 22ce5e3

File tree

2 files changed

+113
-47
lines changed

2 files changed

+113
-47
lines changed

docs/guides/2-essential/11_authentication_devise.md

Lines changed: 42 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ class Admin::App < Matestack::Ui::App
136136

137137
def logout_action_config
138138
{
139-
method: :delete,
139+
method: :get,
140140
path: destroy_admin_session_path,
141141
success: {
142142
redirect: {
@@ -180,7 +180,7 @@ While it's similar to the `Demo::App`, the `Admin::App` does have some differenc
180180
if admin_signed_in?
181181
```
182182

183-
There is also a logout button, using an `action` compoent.
183+
There is also a logout button, using an `action` component.
184184

185185
We could now use the `Admin::App` as layout, but we need to set it with `matestack_app` in the corresponding controller and we need to include our new registry with `include Admin::Component::Registry`.
186186

@@ -581,7 +581,7 @@ class Admin::PersonsController < Admin::BaseController
581581
end
582582
```
583583

584-
We added all required actions according to our routes. Did you notice our controller now inherits from a `Admin::BaseController`? We need to create it in the next step, but why did we create one? Because all routes and corresponding actions belonging to the admin should not be visible without logging in as admin. Therefore we implement a `before_action` hook which calls devise `authenticate_admin!` helper, making sure that every action can only be called by a logged in admin. We could do this in our persons controller, but as we might add other controllers later they only need to inherit from our base controller and are also protected. Let's create the base controller in `app/controllers/admin/base_controller.rb`.
584+
We added all required actions according to our routes. Did you notice our controller now inherits from `Admin::BaseController`? We need to create it in the next step, but why did we create one? Because all routes and corresponding actions belonging to the admin should not be visible without logging in as admin. Therefore we implement a `before_action` hook which calls devise `authenticate_admin!` helper, making sure that every action can only be called by a logged in admin. We could do this in our persons controller, but as we might add other controllers later they only need to inherit from our base controller and are also protected. Let's create the base controller in `app/controllers/admin/base_controller.rb`.
585585

586586
```ruby
587587
class Admin::BaseController < ApplicationController
@@ -592,8 +592,9 @@ class Admin::BaseController < ApplicationController
592592
end
593593
```
594594

595-
In order for devise to use our sign in page, we need to create a custom session controller. Also we need to override the create and delete action of devise session controller, because we would else get errors or unwanted behavior. If we don't override the `create` action devise will trigger a full website reload and rerendering our sign in page, which means we couldn't handle login errors dynamically. The `delete` action needs to be overriden because devise usual redirect will not work with matestack. See below on how to create the session controller for devise and don't forget to update the routes in order to tell devise to use the correct controller.
595+
In order for devise to use our sign in page, we need to create a custom session controller. We also specify a path admins should be redirected to after sign out.
596596

597+
See below on how to create the session controller for devise.
597598

598599
`app/controllers/admin/sessions_controller.rb`
599600
```ruby
@@ -607,21 +608,45 @@ class Admin::SessionsController < Devise::SessionsController
607608
render Admin::Pages::Sessions::SignIn
608609
end
609610

610-
def create
611-
self.resource = warden.authenticate(auth_options)
612-
return render json: {}, status: 401 unless resource
613-
sign_in(resource_name, resource)
614-
respond_with resource, location: after_sign_in_path_for(resource)
611+
private
612+
613+
def after_sign_out_path_for(resource_or_scope)
614+
new_admin_session_path
615615
end
616616

617-
def destroy
618-
signed_out = (Devise.sign_out_all_scopes ? sign_out : sign_out(resource_name))
619-
redirect_to new_admin_session_path, status: :see_other #https://api.rubyonrails.org/classes/ActionController/Redirecting.html
617+
end
618+
```
619+
620+
We also need to create a custom failure app to return the expected response for login attempts with wrong credentials.
621+
622+
`lib/devise/json_failure_app.rb`
623+
```ruby
624+
class JsonFailureApp < Devise::FailureApp
625+
626+
def respond
627+
return super unless request.content_type == 'application/json'
628+
self.status = 401
629+
self.content_type = :json
620630
end
621631

622632
end
623633
```
624634

635+
And tell devise to use it by requiring it in the initializer and updating the config. We also need to update the `sign_out_via` configuration parameter to use http GET requests for sign out instead of DELETE.
636+
637+
`config/intializers/devise.rb`
638+
```ruby
639+
require "#{Rails.root}/lib/devise/json_failure_app"
640+
641+
#...
642+
643+
config.warden do |manager|
644+
manager.failure_app = JsonFailureApp
645+
end
646+
```
647+
648+
Finally remember to update the routes in order to tell devise to use the correct controller.
649+
625650
`config/routes.rb`
626651
```ruby
627652
Rails.application.routes.draw do
@@ -644,6 +669,8 @@ Rails.application.routes.draw do
644669
end
645670
```
646671

672+
If you want more information on why these changes and configurations are necessary take a look at our [devise guide](/docs/guides/5-authorization_authentication/devise.md).
673+
647674
But if you try to start your application locally, visiting the admin pages doesn't work yet - what's going on?
648675

649676
### Styling, Layout & JavaScript
@@ -853,10 +880,12 @@ git commit -m "Add admin login to DemoApp, add default :role to Person model, re
853880

854881
What exactly is going on under the hood with all the admin sign in stuff, you may wonder?
855882

856-
Here's a quick overview: Instead over implementing loads of (complex) functionality with a load of implications and edge cases, we use the `Devise` gem for a rock-solid authentication. It takes care of hashing, salting and storing the password, and through the `Devise::SessionsController`, of managing the session cookie. All that's left for us to do is check for the existence of said cookie by using the `authenticate_admin!` helper. If the required cookie is not present, the controller responds with an error code.
883+
Here's a quick overview: Instead of implementing loads of (complex) functionality with a load of implications and edge cases, we use the `Devise` gem for a rock-solid authentication. It takes care of hashing, salting and storing the password and managing session cookies. All that's left for us to do is check for authentication of admins by using the `authenticate_admin!` helper.
857884

858885
`Devise` could do a lot more, but as this is a basic guide, we will leave it with that. For even more fine-grained control over access rights (authorization) within your application (e.g. by introducing a superadmin or having regional and national manager roles), we recommend to take a look at two other popular Ruby/Rails gems, [Pundit](https://github.com/varvet/pundit) and [CanCanCan](https://github.com/CanCanCommunity/cancancan).
859886

887+
If you want to know more about using devise with matestack, checkout our [devise guide](/docs/guides/5-authorization_authentication/devise.md).
888+
860889
## Creating a admin
861890

862891
In order to use the admin app we need to create an admin with credentials which we can now use to sign in.

docs/guides/5-authorization_authentication/devise.md

Lines changed: 71 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,14 @@ Devise is one of the most popular gems for authentication. Find out more about D
44

55
In order to integrate it fully in matestack apps and pages we need to adjust a few things. This guide explains what exactly needs to be adjusted.
66

7+
## Table of contents
8+
9+
1. [Setting up devise](#setting-up-devise)
10+
1. [Devise models](#devise-models)
11+
1. [Devise helpers](#devise-helpers)
12+
1. [Devise sign in](#devise-sign-in)
13+
1. [Devise sign out](#devise-sign-out)
14+
715
## Setting up Devise
816

917
We install devise by following devise [installation guide](https://github.com/plataformatec/devise/#getting-started).
@@ -19,7 +27,7 @@ config.action_mailer.default_url_options = {
1927
}
2028
```
2129

22-
## Devise Model
30+
## Devise models
2331

2432
Generating a devise model or updating an existing one works as usual. Just use devise generator to create a model. If you already have a model which you want to extend for use with devise take a look at the devise documentation on how to do so.
2533

@@ -30,7 +38,7 @@ rails generate devise user
3038

3139
And ensure `devise_for :user` is added to the `app/config/routes.rb`
3240

33-
## Devise Helpers
41+
## Devise helpers
3442

3543
Again nothing unusual here. We can access devise helper methods inside our controllers, apps, pages and components like we would normally do. In case of our user model this means we could access `current_user` or `user_signed_in?` in apps, pages and components.
3644

@@ -59,26 +67,26 @@ class ExampleController < ApplicationController
5967
end
6068
```
6169

62-
## Devise Login
70+
## Devise sign in
6371

64-
Using the default devise login views should work without a problem, but they will not be integrated inside a matestack app. Let's assume we have a profile matestack app called `Profile::App`. If we want to take advantage of matestacks transitions features (not reloading our app layout between page transitions) we can not use devise views, because we would need to redirect to them and therefore need to reload the whole page. Requiring us for example to implement our navigation twice. In our `Profile::App` and also in the our devise sign in view.
72+
Using the default devise sign in views should work without a problem, but they will not be integrated with a matestack app. Let's assume we have a profile matestack app called `Profile::App`. If we want to take advantage of matestacks transitions features (not reloading our app layout between page transitions) we can not use devise views, because we would need to redirect to them and therefore need to reload the whole page. Requiring us for example to implement our navigation twice. In our `Profile::App` and also in our devise sign in view.
6573

66-
Therefore we need to adjust a few things and create some pages. First we need a custom sign in page containing a form with email and password inputs.
74+
Therefore we need to adjust a few things and create some pages. First we create a custom sign in page containing a form with email and password inputs.
6775

6876
`app/matestack/profile/pages/sessions/sign_in.rb`
6977
```ruby
7078
class Profile::Pages::Sessions::SignIn < Matestack::Ui::Page
7179

7280
def response
73-
heading text: 'Login'
81+
heading text: 'Sign in'
7482
form form_config do
7583
form_input label: 'Email', key: :email, type: :email
7684
form_input label: 'Password', key: :password, type: :password
7785
form_submit do
78-
button text: 'Login'
86+
button text: 'Sign in'
7987
end
8088
end
81-
toggl show_on: 'login_failure' do
89+
toggl show_on: 'sign_in_failure' do
8290
'Your email or password is not valid.'
8391
end
8492
end
@@ -95,16 +103,18 @@ class Profile::Pages::Sessions::SignIn < Matestack::Ui::Page
95103
}
96104
},
97105
failure: {
98-
emit: 'login_failure'
106+
emit: 'sign_in_failure'
99107
}
100108
end
101109

102110
end
103111
```
104112

105-
This page displays a form with a email and password input. The default required parameters for a devise login. It also contains a `toggle` component which gets shown when the event `login_failure` is emitted. This event gets emitted in case our form submit was unsuccessful as we specified it in our `form_config` hash. If the form is successful our app will make a transition to the page the server would redirect to.
113+
This page displays a form with an email and password input. The default required parameters for a devise sign in. It also contains a `toggle` component which gets shown when the event `sign_in_failure` is emitted. This event gets emitted in case our form submit was unsuccessful as we specified it in our `form_config` hash. If the form is successful our app will make a transition to the page the server would redirect to.
114+
115+
Remember to use `redirect` instead of `transition`, if you have conditional content depending on a logged in user inside your app. You have to use `redirect` because the app needs to be reloaded, which happens only with `redirect`.
106116

107-
In order to render our sign in page when someone tries to access a route which needs authentication or someone visits the sign in page we must override devise session controller in order to render this page. We do this by configuring our routes to use a custom controller.
117+
In order to render our sign in page when someone tries to access a route which needs authentication or visits the sign in page we must override devise session controller in order to render our page. We do this by configuring our routes to use a custom controller.
108118

109119
`app/config/routes.rb`
110120
```ruby
@@ -117,7 +127,7 @@ Rails.application.routes.draw do
117127
end
118128
```
119129

120-
Override the `new` action in order to render our sign in page.
130+
Override the `new` action in order to render our sign in page and set the correct matestack app in the controller. Also remember to include the components registry. This is necessary if you use custom components in your app or page, because without it matestack can't resolve them.
121131

122132
`app/controllers/users/sessions_controller.rb`
123133
```ruby
@@ -135,43 +145,70 @@ class Users::SessionController < Devise::SessionController
135145
end
136146
```
137147

138-
Finally we need to override the create method in order to fully leverage matestacks potential. Matestack expects to retrieve a json response with a html error code if the sign in has failed due to matestacks form error handling. To achieve this we need to override the `create` method as you can see below:
148+
Now our sign in is nearly complete. Logging in with correct credentials works fine, but logging in with incorrect credentials triggers a page reload and doesn't show our error message.
149+
150+
Devise usually responds with a 401 for wrong credentials but intercepts this response and redirects to the new action. This means our `form` component recieves the response of the `new` action, which would have a success status. Therefore it redirects you resulting in a rerendering of the sign in page. So our `form` component needs to recieve a error code in order to work as expected. To achieve this we need to provide a custom failure app.
151+
152+
Create the custom failure app under `lib/devise/json_failure_app.rb` containing following code:
139153

140154
```ruby
141-
def create
142-
self.resource = warden.authenticate(auth_options)
143-
return render json: {}, status: 401 unless resource
144-
sign_in(resource_name, resource)
145-
respond_with resource, location: after_sign_in_path_for(resource)
155+
class JsonFailureApp < Devise::FailureApp
156+
157+
def respond
158+
return super unless request.content_type == 'application/json'
159+
self.status = 401
160+
self.content_type = :json
161+
end
162+
146163
end
147164
```
148165

149-
We stayed as close to devise implementation as possible. The important part is line 3 where we return a json response with error code 401 if warden couldn't authenticate the resource.
166+
We only want to overwrite the behavior of the failure app for request with `application/json` as content type, setting the status to a 401 unauthorized error and the content_type to json.
150167

151-
**Wrap Up**
152-
That's it. Now you have a working fully integrated login with devise and matestack. All we needed to do was to create a sign in page, update our routes to use a custom session controller and override two methods in this controller.
168+
There is only one thing left, telling devise to use our custom failure app. Therefore add/update the following lines in `config/initializers/devise.rb`.
153169

154-
## Devise logout
170+
```ruby
171+
require "#{Rails.root}/lib/devise/json_failure_app"
155172

156-
----
157-
TODO devise logout, registration etc.
158-
----
173+
config.warden do |manager|
174+
manager.failure_app = JsonFailureApp
175+
end
176+
```
159177

178+
That's it. When we now try to sign in with incorrect credentials the `form` component triggers the `sign_in_failure` event, which sets off our `toggle` component resulting in displaying the error message.
160179

180+
**Wrap Up**
181+
That's it. Now you have a working sign in with devise fully integrated into matestack. All we needed to do was creating a sign in page, updating our routes to use a custom session controller, overriding the new action, creating a custom failure app and updating the devise config.
161182

162183

163-
## Example
184+
## Devise sign out
164185

165-
This is just your average Rails user controller. The `before_action` gets called on initial pageload and on every subsequent AJAX request the client sends.
186+
Creating a sign out button in matestack is very straight forward. We use matestacks [`action` component](/docs/api/2-components/action.md) to create a sign out button. See the example below:
166187

167188
```ruby
168-
class UserController < ApplicationController
169-
before_action :authenticate_user! # Devise authentication
170-
matestack_app UserApp
189+
action sign_out_config do
190+
button text: 'Sign out'
191+
end
192+
```
193+
```ruby
194+
def sign_out_config
195+
{
196+
method: :get,
197+
path: destroy_admin_session_path,
198+
success: {
199+
transition: {
200+
follow_response: true
201+
}
202+
}
203+
}
204+
end
205+
```
171206

172-
def show
173-
render UserApp::Pages::Show # matestack page responder
174-
end
207+
Notice the `method: :get` in the configuration hash. We use a http GET request to sign out, because the browser will follow the redirect send from devise session controller and then matestack tries to load the page where we have been redirected to. When we would use a DELETE request the action we would be redirected to from the browser will be also requested with a http DELETE request, which will result in a rails routing error. Therefore we use GET and need to configure devise accordingly by changing the `sign_out_via` configuration parameter.
175208

176-
end
209+
```ruby
210+
# The default HTTP method used to sign out a resource. Default is :delete.
211+
config.sign_out_via = :get
177212
```
213+
214+
That's all we have to do.

0 commit comments

Comments
 (0)