-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathrails_authentication.html
More file actions
1041 lines (996 loc) · 68.9 KB
/
rails_authentication.html
File metadata and controls
1041 lines (996 loc) · 68.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<!doctype html>
<html dir="ltr" lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Basic Authentication in Rails — Ruby on Rails Guides</title>
<link rel="stylesheet" type="text/css" href="stylesheets/style.css" data-turbo-track="reload">
<link rel="stylesheet" type="text/css" href="stylesheets/print.css" media="print">
<link rel="stylesheet" type="text/css" href="stylesheets/highlight.css" data-turbo-track="reload">
<link rel="icon" href="images/backend-development.svg" sizes="any">
<script src="javascripts/@hotwired--turbo.js" data-turbo-track="reload"></script>
<script src="javascripts/clipboard.js" data-turbo-track="reload"></script>
<script src="javascripts/guides.js" data-turbo-track="reload"></script>
<meta property="og:title" content="Basic Authentication in Rails — Ruby on Rails Guides" />
<meta name="description" content="Basic Authentication in RailsThis guide is about simple Logins and Logouts for your Rails app.After reading this guide you will understand how cookies, sessions, and logins are connected be able to build a rails app with simple login and logout be able to offer password reminders to your users You can study the code and try out the demos for the authentication examples described here." />
<meta property="og:description" content="Basic Authentication in RailsThis guide is about simple Logins and Logouts for your Rails app.After reading this guide you will understand how cookies, sessions, and logins are connected be able to build a rails app with simple login and logout be able to offer password reminders to your users You can study the code and try out the demos for the authentication examples described here." />
<meta property="og:locale" content="en_US" />
<meta property="og:site_name" content="Textbook Backend Developemnt" />
<meta property="og:image" content="images/backend-development.svg" />
<meta property="og:type" content="website" />
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+Arabic:wght@100..900&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Heebo:wght@100..900&family=Noto+Sans+Arabic:wght@100..900&display=swap" rel="stylesheet">
<meta name="theme-color" content="#2e56e9">
</head>
<body class="guide">
<header id="page_header">
<div class="wrapper clearfix">
<nav id="feature_nav">
<div class="header-logo">
<a href="/">Backend Development</a>
</div>
<ul class="nav">
<li><a class="nav-item" id="home_nav" href="/">Home</a></li>
<li class="guides-index guides-index-large">
<a href="index.html" id="guidesMenu" class="guides-index-item nav-item">Index</a>
<div id="guides" class="clearfix" style="display: none;">
<hr />
<dl class="guides-section-container">
<div class="guides-section">
<dt>Ruby on Rails</dt>
<dd><a href="ruby_commandline.html">Ruby Commandline</a></dd>
<dd><a href="rails_database_and_model.html">Models and ActiveRecord</a></dd>
<dd><a href="rails_db.html">Database and Migrations</a></dd>
<dd><a href="rails_associations_and_validations.html">Associations and Validations</a></dd>
<dd><a href="rails_view_and_controller.html">Routing, View and Controller</a></dd>
<dd><a href="rails_authentication.html">Simple Authentication</a></dd>
<dd><a href="assets_and_import_map.html">The Asset Pipeline and Importmaps</a></dd>
<dd><a href="testing.html">Getting started with Testing</a></dd>
<dd><a href="refactoring_rails.html">Refactoring Rails</a></dd>
<dd><a href="deploy-to-paas.html">Deploy to PAAS</a></dd>
<dd><a href="rails_gems.html">Ruby Gems for your Rails Project</a></dd>
<dd><a href="deploying_rails.html">Deploying Rails</a></dd>
</div>
<div class="guides-section">
<dt>Ruby on Rails - Advanced Topics</dt>
<dd><a href="deploy-to-paas.html">Deploy to PAAS</a></dd>
<dd><a href="rest-api.html">REST API</a></dd>
<dd><a href="graphql-api.html">GraphQL API</a></dd>
<dd><a href="rails_websockets.html">Websocket in Rails</a></dd>
<dd><a href="jobs_and_tasks.html">Jobs and Tasks in Rails</a></dd>
<dd><a href="rails_security.html">Rails Security</a></dd>
</div>
<div class="guides-section">
<dt>Overarching Concerns</dt>
<dd><a href="issue.html">Issue Lifecycle</a></dd>
<dd><a href="security.html">Security</a></dd>
<dd><a href="adv_authentication.html">Advanced Authentication</a></dd>
<dd><a href="caching.html">Caching</a></dd>
<dd><a href="advanced_testing.html">Advanced Testing</a></dd>
<dd><a href="internationalization.html">Internationalization (I18n)</a></dd>
<dd><a href="git_rebasing.html">Git Rebasing</a></dd>
</div>
<div class="guides-section">
<dt>Nodes.js</dt>
<dd><a href="node_vs_rails.html">Node vs. Rails</a></dd>
<dd><a href="node_basics.html">Node Basics</a></dd>
<dd><a href="node_websockets.html">Node Websockets</a></dd>
<dd><a href="node_express.html">Node Web App</a></dd>
<dd><a href="node_cluster.html">Scaling Node</a></dd>
</div>
<div class="guides-section">
<dt>Next.js</dt>
<dd><a href="nextjs.html">Next.js</a></dd>
</div>
</dl>
</div>
</li>
<li class="guides-index guides-index-small">
<select class="guides-index-item nav-item">
<option value="index.html">Index</option>
<optgroup label="Ruby on Rails">
<option value="ruby_commandline.html">Ruby Commandline</option>
<option value="rails_database_and_model.html">Models and ActiveRecord</option>
<option value="rails_db.html">Database and Migrations</option>
<option value="rails_associations_and_validations.html">Associations and Validations</option>
<option value="rails_view_and_controller.html">Routing, View and Controller</option>
<option value="rails_authentication.html">Simple Authentication</option>
<option value="assets_and_import_map.html">The Asset Pipeline and Importmaps</option>
<option value="testing.html">Getting started with Testing</option>
<option value="refactoring_rails.html">Refactoring Rails</option>
<option value="deploy-to-paas.html">Deploy to PAAS</option>
<option value="rails_gems.html">Ruby Gems for your Rails Project</option>
<option value="deploying_rails.html">Deploying Rails</option>
</optgroup>
<optgroup label="Ruby on Rails - Advanced Topics">
<option value="deploy-to-paas.html">Deploy to PAAS</option>
<option value="rest-api.html">REST API</option>
<option value="graphql-api.html">GraphQL API</option>
<option value="rails_websockets.html">Websocket in Rails</option>
<option value="jobs_and_tasks.html">Jobs and Tasks in Rails</option>
<option value="rails_security.html">Rails Security</option>
</optgroup>
<optgroup label="Overarching Concerns">
<option value="issue.html">Issue Lifecycle</option>
<option value="security.html">Security</option>
<option value="adv_authentication.html">Advanced Authentication</option>
<option value="caching.html">Caching</option>
<option value="advanced_testing.html">Advanced Testing</option>
<option value="internationalization.html">Internationalization (I18n)</option>
<option value="git_rebasing.html">Git Rebasing</option>
</optgroup>
<optgroup label="Nodes.js">
<option value="node_vs_rails.html">Node vs. Rails</option>
<option value="node_basics.html">Node Basics</option>
<option value="node_websockets.html">Node Websockets</option>
<option value="node_express.html">Node Web App</option>
<option value="node_cluster.html">Scaling Node</option>
</optgroup>
<optgroup label="Next.js">
<option value="nextjs.html">Next.js</option>
</optgroup>
</select>
</li>
</ul>
</nav>
</div>
</header>
<hr class="hide" />
<section id="feature">
<div class="wrapper">
<h1>Basic Authentication in Rails</h1><p>This guide is about simple Logins and Logouts for your Rails app.</p><p>After reading this guide you will</p>
<ul>
<li>understand how cookies, sessions, and logins are connected</li>
<li>be able to build a rails app with simple login and logout</li>
<li>be able to offer password reminders to your users</li>
</ul>
<div class="interstitial repo"><p>You can study the <a href="https://github.com/backend-development/rails-example-kanban-board-login">code</a> and try out <a href="https://kanban-1.projects.multimediatechnology.at/">the demos</a> for the authentication examples described here.</p></div>
<nav id="subCol">
<h3 class="chapter">
<picture>
<!-- Using the `source` HTML tag to set the dark theme image -->
<source
srcset="images/icon_book-close-bookmark-1-wht.svg"
media="(prefers-color-scheme: dark)"
/>
<img src="images/icon_book-close-bookmark-1.svg" alt="Chapter Icon" />
</picture>
Chapters
</h3>
<ol class="chapters">
<li><a href="#http-and-sessions">HTTP and Sessions</a>
<ul>
<li><a href="#how-to-add-state-to-http">How to add state to HTTP</a></li>
<li><a href="#security">Security</a></li>
<li><a href="#session-in-backend-development">Session in Backend Development</a></li>
</ul></li>
<li><a href="#authentication">Authentication</a>
<ul>
<li><a href="#password-storage">Password Storage</a></li>
<li><a href="#has-secure-password">has secure password</a></li>
<li><a href="#validates-confirmation-of-password">validates confirmation of password</a></li>
</ul></li>
<li><a href="#basic-login">Basic Login</a>
<ul>
<li><a href="#routes">Routes</a></li>
<li><a href="#controller">Controller</a></li>
<li><a href="#login-view">Login View</a></li>
<li><a href="#session-controller">Session Controller</a></li>
<li><a href="#helpers">Helpers</a></li>
</ul></li>
<li><a href="#better-login-ux">Better Login UX</a></li>
<li><a href="#oauth-and-openid-connect">OAuth and OpenID Connect</a>
<ul>
<li><a href="#rails-and-oauth">Rails and OAuth</a></li>
<li><a href="#providers">Providers</a></li>
<li><a href="#models">Models</a></li>
<li><a href="#login-and-logout">Login and Logout</a></li>
</ul></li>
<li><a href="#see-also">See Also</a></li>
<li><a href="#further-reading">Further Reading</a></li>
</ol>
</nav>
<hr>
</div>
</section>
<main id="container">
<div class="wrapper">
<div id="mainCol">
<div class='slide'>
</div>
<div class='slide'>
<p class='slide_break_block'><a class='slide_break' id='slide-0' href='slides_rails_authentication.html#/0'>◻</a></p>
<h2 id="http-and-sessions"><a class="anchorlink" href="#http-and-sessions"><span>1</span> HTTP and Sessions</a></h2><p>HTTP is a <strong>stateless</strong> protocol. This means that the protocol
does not require the web server to remember anything from one
request to the next. So calling the same URL from different
clients will basically return the same result.</p><p>But this is not enough for many web apps we want to build: we
want certain pages to only be available to some users. We want
to offer shopping carts or wizards that let a user complete
a complex action through several small steps, that carry over state.</p><p><img src="images/session-cart.jpg" alt="cookie set by rails, as displayed by firebug"></p></div>
<div class='slide'>
<p class='slide_break_block'><a class='slide_break' id='slide-1' href='slides_rails_authentication.html#/1'>◻</a></p>
<p>In an old style <strong>GUI application</strong> running on windows or mac it
is clear that that only one user is using the app at a time. We
can use variables in main memory to store information pertaining
to that user, and they will carry over through many interactions
(opening a new window of our app, clicking a button, selecting
something from a menu).</p><p>In a <strong>web app</strong> this is true for the frontend of the app, but only in a very
limited sense: If you set a variable in javascript it will only
be available for this one user in this one webbrowser.
But if the user leaves your app by typing in a new URL,
or following a link or just reloading the page this information will be lost.</p><p>In the backend we need some way to identify that a certain
request comes from a certain user, and to carry over the state
from one HTTP request to the next.</p></div>
<div class='slide'>
<p class='slide_break_block'><a class='slide_break' id='slide-2' href='slides_rails_authentication.html#/2'>◻</a></p>
<h3 id="how-to-add-state-to-http"><a class="anchorlink" href="#how-to-add-state-to-http"><span>1.1</span> How to add state to HTTP</a></h3><p>There are several ways to do this:</p>
<ol>
<li> <strong>HTTP Basic Authentication</strong> according to <a href="https://tools.ietf.org/html/rfc1945#section-11">rfc 1945, section 11</a>: The server sends a <code>WWW-Authenticate: Basic ...</code> header in the first response. The browser asks the user for username and password, and then sends the (hashed) username and password to the server with subsequent request using the HTTP Headers <code>Authorization: Basic ...</code>.</li>
<li> <strong>HTTP Cookies</strong> according to <a href="https://tools.ietf.org/html/rfc6265">rfc 6265</a>. The server sets the cookie (using the Header 'Set-Cookie'), the client returns the cookie automatically for every subsequent request to the server (using the HTTP Header <code>Cookie</code>).</li>
<li> <strong>JSON-Web-Token</strong> according to <a href="https://jwt.io/">jwt.io</a> / <a href="https://tools.ietf.org/html/rfc7519">rfc 7519</a> use a can be used in three ways:</li>
</ol>
<ul>
<li>directly in HTTP with <code>Authorization: Bearer ...</code> and <code>WWW-Authenticate: Bearer ...</code></li>
<li>as a parameter in an URL</li>
<li>as POST data</li>
</ul>
</div>
<div class='slide'>
<p class='slide_break_block'><a class='slide_break' id='slide-3' href='slides_rails_authentication.html#/3'>◻</a></p>
<h3 id="security"><a class="anchorlink" href="#security"><span>1.2</span> Security</a></h3><p>If you use any of these methods over HTTP, unencrypted,
then an attacker might be able to steal the authentication information.
So always use HTTPS!</p><p>Both Authenticate-Headers and Cookies are sent automatically
by the browser each time you access the web app. This can be used
exploited by <a href="https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF)">Cross Site Request Forgery attacks</a>.</p></div>
<div class='slide'>
<p class='slide_break_block'><a class='slide_break' id='slide-4' href='slides_rails_authentication.html#/4'>◻</a></p>
<h3 id="session-in-backend-development"><a class="anchorlink" href="#session-in-backend-development"><span>1.3</span> Session in Backend Development</a></h3><p>Web frameworks use any of the methods described above
to offer so called <strong>sessions</strong> to the
developer: a session is a key-value store that is associated with
the requests of one specific user.</p><p>Ruby on Rails by default sets a cookie named after the application.
In the screenshot below you can see the cookie set by the <code>kanban</code>
application as displayed by firefox developer tools in the
tab <strong>storage</strong>.</p><p><img src="images/cookie-in-ff-inspector.png" alt="cookie set by rails, as displayed by firefox develoepr tools "></p></div>
<div class='slide'>
<p class='slide_break_block'><a class='slide_break' id='slide-5' href='slides_rails_authentication.html#/5'>◻</a></p>
<p><strong>A Note on Security</strong></p><p>The cookie is set with the <code>HttpOnly</code> option, which means it cannot be
changed by JavaScript in the browser. But it is still vulnerable to a
replay attack: we can read out the cookie in the developer tools.
Using <code>curl</code> on the command line we can send the stolen
cookie with a HTTP request and will be 'logged in' for that request:</p><div class="interstitial code">
<pre><code class="highlight plaintext">curl -v --cookie "_kanban_session=bWdwc...d4c; path=/; HttpOnly" https://kanban-2.herokuapp.com/
...
<span>Logged in as mariam <a href="/logout">logout</a>
</code></pre>
<button class="clipboard-button" data-clipboard-text="curl -v --cookie "_kanban_session=bWdwc...d4c; path=/; HttpOnly" https://kanban-2.herokuapp.com/
...
<span>Logged in as mariam <a href="/logout">logout</a>
">Copy</button>
</div>
<p>This makes it all the more important that the cookie can not be stolen!
Remember to <strong>always use https</strong> if your app authenticates users at
any point.</p></div>
<div class='slide'>
<p class='slide_break_block'><a class='slide_break' id='slide-6' href='slides_rails_authentication.html#/6'>◻</a></p>
<p>The Rails framework automatically sets and reads this cookie,
and offers a Hash <code>session</code> that is accessible from
both controllers and views. By default the keys and values you store
in the session hash are serialized, encrypted with a secret key and
sent as the value of the session cookie.</p><p>The cookie is not saved on the server.</p><p>This way of handling state works well with load balancers and
multiple web servers.</p></div>
<div class='slide'>
<p class='slide_break_block'><a class='slide_break' id='slide-7' href='slides_rails_authentication.html#/7'>◻</a></p>
<p>Even without authentication, you can use the session to track a user
as they browse through the web app. For example you could count
how many requests they have already made:</p><div class="interstitial code">
<pre><code class="highlight ruby"><span class="c1"># in app/controllers/application_controller.rb</span>
<span class="n">before_action</span> <span class="ss">:count_requests</span>
<span class="k">def</span> <span class="nf">count_requests</span>
<span class="n">session</span><span class="p">[</span><span class="ss">:counter</span><span class="p">]</span> <span class="o">=</span> <span class="mi">0</span> <span class="k">if</span> <span class="n">session</span><span class="p">[</span><span class="ss">:counter</span><span class="p">].</span><span class="nf">nil?</span>
<span class="n">session</span><span class="p">[</span><span class="ss">:counter</span><span class="p">]</span> <span class="o">+=</span> <span class="mi">1</span>
<span class="k">end</span>
</code></pre>
<button class="clipboard-button" data-clipboard-text="# in app/controllers/application_controller.rb
before_action :count_requests
def count_requests
session[:counter] = 0 if session[:counter].nil?
session[:counter] += 1
end
">Copy</button>
</div>
<p>And show this number to the user</p><div class="interstitial code">
<pre><code class="highlight ruby"><span class="c1"># in app/views/layouts/application.html.erb</span>
<span class="n">you</span> <span class="n">have</span> <span class="n">made</span> <span class="o"><</span><span class="sx">%= session[:counter] %> requests
in session <%=</span> <span class="n">session</span><span class="p">.</span><span class="nf">id</span> <span class="sx">%>
</span></code></pre>
<button class="clipboard-button" data-clipboard-text="# in app/views/layouts/application.html.erb
you have made <%= session[:counter] %> requests
in session <%= session.id %>
">Copy</button>
</div>
<p>See <a href="https://guides.rubyonrails.org/action_controller_overview.html#session">Rails Guide: Controller</a>
for more details.</p></div>
<div class='slide'>
<p class='slide_break_block'><a class='slide_break' id='slide-8' href='slides_rails_authentication.html#/8'>◻</a></p>
<h2 id="authentication"><a class="anchorlink" href="#authentication"><span>2</span> Authentication</a></h2><p>The session lets you recognize the same user from one HTTP
request to the next. But it does not - in itself - help to authenticate users.</p><p>The most common way to achieve authentication is through passwords.
For simple authentication with passwords there is a generator in Rails 8:</p><div class="interstitial code">
<pre><code class="highlight ruby"><span class="n">rails</span> <span class="n">generate</span> <span class="n">authentication</span>
</code></pre>
<button class="clipboard-button" data-clipboard-text="rails generate authentication
">Copy</button>
</div>
</div>
<div class='slide'>
<p class='slide_break_block'><a class='slide_break' id='slide-9' href='slides_rails_authentication.html#/9'>◻</a></p>
<h3 id="password-storage"><a class="anchorlink" href="#password-storage"><span>2.1</span> Password Storage</a></h3><p>When storing passwords in a web app there are a lot of things you
can do wrong: store the password as plain text, for example.</p><p>Rails comes with built in functions for handling passwords,
which go a long way to following the <a href="https://www.owasp.org/index.php/Password_Storage_Cheat_Sheet">OWASP recommendations</a>.</p><p>Rails assumes that you have added <code>bcrypt</code> to the Gemfile
and have an attribute <code>password_digest</code>
in your model (and no attribute <code>password</code>).</p></div>
<div class='slide'>
<p class='slide_break_block'><a class='slide_break' id='slide-10' href='slides_rails_authentication.html#/10'>◻</a></p>
<h3 id="has-secure-password"><a class="anchorlink" href="#has-secure-password"><span>2.2</span> has secure password</a></h3><p>Add <code>bcrypt</code> to your Gemfile and <code>bundle install</code>.</p><p>Then add <code>has_secure_password</code> to your user model:</p><div class="interstitial code">
<pre><code class="highlight ruby"><span class="k">class</span> <span class="nc">User</span> <span class="o"><</span> <span class="no">ApplicationRecord</span>
<span class="n">has_secure_password</span>
</code></pre>
<button class="clipboard-button" data-clipboard-text="class User < ApplicationRecord
has_secure_password
">Copy</button>
</div>
<p>Now if you call</p><div class="interstitial code">
<pre><code class="highlight ruby"><span class="no">User</span><span class="p">.</span><span class="nf">create</span><span class="p">({</span><span class="ss">username: </span><span class="s2">"mariam"</span><span class="p">,</span> <span class="ss">password: </span><span class="s1">'badpassword123'</span> <span class="p">})</span>
</code></pre>
<button class="clipboard-button" data-clipboard-text="User.create({username: "mariam", password: 'badpassword123' })
">Copy</button>
</div>
<p>The password will be encrypted, and only the encrypted version
will be stored in the database, in attribute <code>password_digest</code>.</p><p>It will add the <code>authenticate</code> method to your User model:</p><div class="interstitial code">
<pre><code class="highlight ruby"><span class="n">user</span> <span class="o">=</span> <span class="no">User</span><span class="p">.</span><span class="nf">find_by</span><span class="p">(</span><span class="ss">username: </span><span class="s2">"mariam"</span><span class="p">).</span><span class="nf">authenticate</span><span class="p">(</span><span class="s2">"wrong password"</span><span class="p">)</span>
</code></pre>
<button class="clipboard-button" data-clipboard-text="user = User.find_by(username: "mariam").authenticate("wrong password")
">Copy</button>
</div>
<p>The authenticate method will encrypt the password again, and compare
it to the <code>password_digest</code> in the database. It will return nil if
the password does not match.</p></div>
<div class='slide'>
<p class='slide_break_block'><a class='slide_break' id='slide-11' href='slides_rails_authentication.html#/11'>◻</a></p>
<h3 id="validates-confirmation-of-password"><a class="anchorlink" href="#validates-confirmation-of-password"><span>2.3</span> validates confirmation of password</a></h3><p>It is good UX practice to have users supply their
password twice, to make it less likely that typos go through.
Rails also helps you with this: You can add <code>validates_confirmation_of :password</code>
to the user model:</p><div class="interstitial code">
<pre><code class="highlight ruby"><span class="k">class</span> <span class="nc">User</span> <span class="o"><</span> <span class="no">ApplicationRecord</span>
<span class="n">has_secure_password</span>
<span class="n">validates</span> <span class="ss">:password</span><span class="p">,</span> <span class="ss">confirmation: </span><span class="kp">true</span>
</code></pre>
<button class="clipboard-button" data-clipboard-text="class User < ApplicationRecord
has_secure_password
validates :password, confirmation: true
">Copy</button>
</div>
<p>Now the create-method is changed again: you need to supply the
passwort twice to the create method:</p><div class="interstitial code">
<pre><code class="highlight ruby"><span class="o">></span> <span class="n">u</span> <span class="o">=</span> <span class="no">User</span><span class="p">.</span><span class="nf">create</span><span class="p">({</span><span class="ss">username: </span><span class="s2">"yusuf"</span><span class="p">,</span>
<span class="ss">password: </span><span class="s1">'badpassword123'</span><span class="p">,</span>
<span class="ss">password_confirmation: </span><span class="s1">'badpassword1234'</span><span class="p">})</span>
<span class="o">=></span> <span class="c1">#<User id: nil, username: "yusuf", password_digest: [FILTERED], created_at: nil, updated_at: nil></span>
<span class="o">></span> <span class="n">u</span><span class="p">.</span><span class="nf">save</span>
<span class="o">=></span> <span class="kp">false</span>
<span class="o">></span> <span class="n">u</span><span class="p">.</span><span class="nf">errors</span><span class="p">.</span><span class="nf">messages</span>
<span class="o">=></span> <span class="p">{</span><span class="ss">:password_confirmation</span><span class="o">=></span><span class="p">[</span><span class="s2">"doesn't match Password"</span><span class="p">]}</span>
</code></pre>
<button class="clipboard-button" data-clipboard-text="> u = User.create({username: "yusuf",
password: 'badpassword123',
password_confirmation: 'badpassword1234'})
=> #<User id: nil, username: "yusuf", password_digest: [FILTERED], created_at: nil, updated_at: nil>
> u.save
=> false
> u.errors.messages
=> {:password_confirmation=>["doesn't match Password"]}
">Copy</button>
</div>
<p>This use of create will not actually succeed, because the password_confirmation does
not match the password.</p></div>
<div class='slide'>
<p class='slide_break_block'><a class='slide_break' id='slide-12' href='slides_rails_authentication.html#/12'>◻</a></p>
<p>This is the minimal user model we need:</p><div class="interstitial code">
<pre><code class="highlight ruby"><span class="k">class</span> <span class="nc">CreateUsers</span> <span class="o"><</span> <span class="no">ActiveRecord</span><span class="o">::</span><span class="no">Migration</span><span class="p">[</span><span class="mf">5.1</span><span class="p">]</span>
<span class="k">def</span> <span class="nf">change</span>
<span class="n">create_table</span> <span class="ss">:users</span> <span class="k">do</span> <span class="o">|</span><span class="n">t</span><span class="o">|</span>
<span class="n">t</span><span class="p">.</span><span class="nf">string</span> <span class="ss">:username</span>
<span class="n">t</span><span class="p">.</span><span class="nf">string</span> <span class="ss">:password_digest</span>
<span class="n">t</span><span class="p">.</span><span class="nf">timestamps</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre>
<button class="clipboard-button" data-clipboard-text="class CreateUsers < ActiveRecord::Migration[5.1]
def change
create_table :users do |t|
t.string :username
t.string :password_digest
t.timestamps
end
end
end
">Copy</button>
</div>
</div>
<div class='slide'>
<p class='slide_break_block'><a class='slide_break' id='slide-13' href='slides_rails_authentication.html#/13'>◻</a></p>
<h2 id="basic-login"><a class="anchorlink" href="#basic-login"><span>3</span> Basic Login</a></h2><p>We now have all the bits and pieces to build a Login with username (or e-mail adress) and password.</p><p>There are some Rails convention around this:</p>
<ul>
<li>the current user should be accessible in controllers and views via a helper method <code>current_user</code>,</li>
<li>logging in and logging out are seen as "creating a session" and "deleting a session" and handled by restful routes,</li>
<li>there is a session controller and some views, but no session model!</li>
</ul>
</div>
<div class='slide'>
<p class='slide_break_block'><a class='slide_break' id='slide-14' href='slides_rails_authentication.html#/14'>◻</a></p>
<h3 id="routes"><a class="anchorlink" href="#routes"><span>3.1</span> Routes</a></h3><p>Let's start by creating the routes:</p><div class="interstitial code">
<pre><code class="highlight ruby"><span class="c1"># config/routes.rb:</span>
<span class="n">get</span> <span class="s1">'/login'</span><span class="p">,</span> <span class="ss">to: </span><span class="s1">'sessions#new'</span> <span class="c1"># show login form</span>
<span class="n">post</span> <span class="s1">'/login'</span><span class="p">,</span> <span class="ss">to: </span><span class="s1">'sessions#create'</span> <span class="c1"># process login</span>
<span class="n">get</span> <span class="s1">'/logout'</span><span class="p">,</span> <span class="ss">to: </span><span class="s1">'sessions#destroy'</span> <span class="c1"># process logout</span>
</code></pre>
<button class="clipboard-button" data-clipboard-text=" get '/login', to: 'sessions#new' # show login form
post '/login', to: 'sessions#create' # process login
get '/logout', to: 'sessions#destroy' # process logout
">Copy</button>
</div>
</div>
<div class='slide'>
<p class='slide_break_block'><a class='slide_break' id='slide-15' href='slides_rails_authentication.html#/15'>◻</a></p>
<h3 id="controller"><a class="anchorlink" href="#controller"><span>3.2</span> Controller</a></h3><p>And the session controller to handle these routes:</p><div class="interstitial code">
<pre><code class="highlight console"><span class="go">rails g controller sessions new create destroy
</span></code></pre>
<button class="clipboard-button" data-clipboard-text="">Copy</button>
</div>
<p>Now you can direct your browser to <a href="http://localhost:3000/login">http://localhost:3000/login</a></p></div>
<div class='slide'>
<p class='slide_break_block'><a class='slide_break' id='slide-16' href='slides_rails_authentication.html#/16'>◻</a></p>
<h3 id="login-view"><a class="anchorlink" href="#login-view"><span>3.3</span> Login View</a></h3><p>Next you need to set up the view for the login form there:</p><div class="interstitial code">
<pre><code class="highlight ruby"><span class="o"><!--</span> <span class="n">app</span><span class="o">/</span><span class="n">views</span><span class="o">/</span><span class="n">sessions</span><span class="o">/</span><span class="n">new</span><span class="p">.</span><span class="nf">html</span><span class="p">.</span><span class="nf">erb</span><span class="p">:</span> <span class="o">--></span>
<span class="o"><</span><span class="n">h1</span><span class="o">></span><span class="no">Log</span> <span class="k">in</span><span class="o"><</span><span class="sr">/h1>
<%= form_with url: login_path, local: true do |f| %>
<div>
<%= f.label :username %>
<%= f.text_field :username %>
</</span><span class="n">div</span><span class="o">></span>
<span class="o"><</span><span class="n">div</span><span class="o">></span>
<span class="o"><</span><span class="sx">%= f.label :password %>
<%=</span> <span class="n">f</span><span class="p">.</span><span class="nf">password_field</span> <span class="ss">:password</span> <span class="o">%></span>
<span class="o"><</span><span class="sr">/div>
<%= submit_tag 'Log In' %>
<% end %>
</span></code></pre>
<button class="clipboard-button" data-clipboard-text="<!-- app/views/sessions/new.html.erb: -->
<h1>Log in</h1>
<%= form_with url: login_path, local: true do |f| %>
<div>
<%= f.label :username %>
<%= f.text_field :username %>
</div>
<div>
<%= f.label :password %>
<%= f.password_field :password %>
</div>
<%= submit_tag 'Log In' %>
<% end %>
">Copy</button>
</div>
</div>
<div class='slide'>
<p class='slide_break_block'><a class='slide_break' id='slide-17' href='slides_rails_authentication.html#/17'>◻</a></p>
<h3 id="session-controller"><a class="anchorlink" href="#session-controller"><span>3.4</span> Session Controller</a></h3><p>This form sends just two pieces of data: <code>username</code> and <code>password</code>.
So in the controller you have to extract these using <code>params.permit</code>.</p><p>If authentication goes through we store the <code>user.id</code> in the session.
Only the <code>id</code> is needed, we can load the rest of the user data
from the database.</p><p>app/app/controllers/sessions_controller.rb:</p><div class="interstitial code">
<pre><code class="highlight ruby"><span class="k">class</span> <span class="nc">SessionsController</span> <span class="o"><</span> <span class="no">ApplicationController</span>
<span class="c1"># displays login form</span>
<span class="k">def</span> <span class="nf">new</span>
<span class="k">end</span>
<span class="c1"># checks login data and starts session</span>
<span class="k">def</span> <span class="nf">create</span>
<span class="n">reset_session</span> <span class="c1"># prevent session fixation</span>
<span class="n">par</span> <span class="o">=</span> <span class="n">login_params</span>
<span class="n">user</span> <span class="o">=</span> <span class="no">User</span><span class="p">.</span><span class="nf">find_by</span><span class="p">(</span><span class="ss">username: </span><span class="n">par</span><span class="p">[</span><span class="ss">:username</span><span class="p">])</span>
<span class="k">if</span> <span class="n">user</span> <span class="o">&&</span> <span class="n">user</span><span class="p">.</span><span class="nf">authenticate</span><span class="p">(</span><span class="n">par</span><span class="p">[</span><span class="ss">:password</span><span class="p">])</span>
<span class="c1"># Save the user id in the session</span>
<span class="c1"># rails will take care of setting + reading cookies</span>
<span class="c1"># when the user navigates around our website.</span>
<span class="n">session</span><span class="p">[</span><span class="ss">:user_id</span><span class="p">]</span> <span class="o">=</span> <span class="n">user</span><span class="p">.</span><span class="nf">id</span>
<span class="n">redirect_to</span> <span class="n">root_path</span><span class="p">,</span> <span class="ss">notice: </span><span class="s1">'Logged in'</span>
<span class="k">else</span>
<span class="n">redirect_to</span> <span class="n">login_path</span><span class="p">,</span> <span class="ss">alert: </span><span class="s1">'Log in failed'</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="c1"># deletes sesssion</span>
<span class="k">def</span> <span class="nf">destroy</span>
<span class="n">reset_session</span>
<span class="n">redirect_to</span> <span class="n">root_path</span><span class="p">,</span> <span class="ss">notice: </span><span class="s1">'Logged out'</span>
<span class="k">end</span>
<span class="kp">private</span>
<span class="k">def</span> <span class="nf">login_params</span>
<span class="n">params</span><span class="p">.</span><span class="nf">permit</span><span class="p">(</span><span class="ss">:username</span><span class="p">,</span> <span class="ss">:password</span><span class="p">,</span> <span class="ss">:password_confirmation</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre>
<button class="clipboard-button" data-clipboard-text="class SessionsController < ApplicationController
# displays login form
def new
end
# checks login data and starts session
def create
reset_session # prevent session fixation
par = login_params
user = User.find_by(username: par[:username])
if user && user.authenticate(par[:password])
# Save the user id in the session
# rails will take care of setting + reading cookies
# when the user navigates around our website.
session[:user_id] = user.id
redirect_to root_path, notice: 'Logged in'
else
redirect_to login_path, alert: 'Log in failed'
end
end
# deletes sesssion
def destroy
reset_session
redirect_to root_path, notice: 'Logged out'
end
private
def login_params
params.permit(:username, :password, :password_confirmation)
end
end
">Copy</button>
</div>
</div>
<div class='slide'>
<p class='slide_break_block'><a class='slide_break' id='slide-18' href='slides_rails_authentication.html#/18'>◻</a></p>
<h3 id="helpers"><a class="anchorlink" href="#helpers"><span>3.5</span> Helpers</a></h3><p>The helper_method <code>current_user</code> we define in
the application controller. If the <code>user_id</code> is not
set in the session or if the user with this <code>user_id</code> does not
exist (any more) we just return <code>nil</code> as the current_user.</p><div class="interstitial code">
<pre><code class="highlight ruby"><span class="o"><!--</span> <span class="n">app</span><span class="o">/</span><span class="n">app</span><span class="o">/</span><span class="n">controllers</span><span class="o">/</span><span class="n">application_controller</span><span class="p">.</span><span class="nf">rb</span> <span class="o">--></span>
<span class="k">def</span> <span class="nf">current_user</span>
<span class="k">if</span> <span class="n">session</span><span class="p">[</span><span class="ss">:user_id</span><span class="p">]</span>
<span class="vi">@current_user</span> <span class="o">||=</span> <span class="no">User</span><span class="p">.</span><span class="nf">where</span><span class="p">(</span><span class="ss">id: </span><span class="n">session</span><span class="p">[</span><span class="ss">:user_id</span><span class="p">]).</span><span class="nf">first</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="n">helper_method</span> <span class="ss">:current_user</span>
</code></pre>
<button class="clipboard-button" data-clipboard-text="<!-- app/app/controllers/application_controller.rb -->
def current_user
if session[:user_id]
@current_user ||= User.where(id: session[:user_id]).first
end
end
helper_method :current_user
">Copy</button>
</div>
</div>
<div class='slide'>
<p class='slide_break_block'><a class='slide_break' id='slide-19' href='slides_rails_authentication.html#/19'>◻</a></p>
<p>With the <code>current_user</code> helper method returning <code>nil</code> if
nobody is logged in we can also use it in the view
to display different things for logged in users and non logged in visitors:</p><div class="interstitial code">
<pre><code class="highlight ruby"><span class="o"><!--</span> <span class="n">app</span><span class="o">/</span><span class="n">views</span><span class="o">/</span><span class="n">layouts</span><span class="o">/</span><span class="n">application</span><span class="p">.</span><span class="nf">html</span><span class="p">.</span><span class="nf">erb</span> <span class="o">--></span>
<span class="o"><</span><span class="sx">% if </span><span class="n">current_user</span> <span class="sx">%>
Logged in as <%= current_user.username %></span>
<span class="o"><</span><span class="sx">%= link_to "log out", logout_path %>
<% else %>
<%=</span> <span class="n">link_to</span> <span class="s2">"log in"</span><span class="p">,</span> <span class="n">login_path</span> <span class="sx">%>
| <%= link_to "register", new_user_path %></span>
<span class="o"><</span><span class="sx">% end </span><span class="o">%></span>
</code></pre>
<button class="clipboard-button" data-clipboard-text="<!-- app/views/layouts/application.html.erb -->
<% if current_user %>
Logged in as <%= current_user.username %>
<%= link_to "log out", logout_path %>
<% else %>
<%= link_to "log in", login_path %>
| <%= link_to "register", new_user_path %>
<% end %>
">Copy</button>
</div>
</div>
<div class='slide'>
<p class='slide_break_block'><a class='slide_break' id='slide-20' href='slides_rails_authentication.html#/20'>◻</a></p>
<p>To register a user - to create a new user - we can
create a full scaffold for the user model, or build
the controller and views by hand.</p><p>We really only need two actions:</p><div class="interstitial code">
<pre><code class="highlight ruby"><span class="c1"># routes.rb</span>
<span class="n">resources</span> <span class="ss">:users</span><span class="p">,</span> <span class="ss">only: </span><span class="sx">%i[new create]</span>
</code></pre>
<button class="clipboard-button" data-clipboard-text="# routes.rb
resources :users, only: %i[new create]
">Copy</button>
</div>
</div>
<div class='slide'>
<p class='slide_break_block'><a class='slide_break' id='slide-21' href='slides_rails_authentication.html#/21'>◻</a></p>
<h2 id="better-login-ux"><a class="anchorlink" href="#better-login-ux"><span>4</span> Better Login UX</a></h2><p>If your app deals with more than just one or two users
that you set up "by hand", the gem <code>devise</code> can help you a lot.
It can makes your logins ...</p>
<ul>
<li><strong>Confirmable</strong>: sends emails with confirmation instructions and verifies whether an account is already confirmed during sign in.</li>
<li><strong>Recoverable</strong>: resets the user password and sends reset instructions.</li>
<li><strong>Registerable</strong>: handles signing up users through a registration process, also allowing them to edit and destroy their account.</li>
<li><strong>Rememberable</strong>: manages generating and clearing a token for remembering the user from a saved cookie.</li>
<li><strong>Trackable</strong>: tracks sign in count, timestamps and IP address.</li>
<li><strong>Timeoutable</strong>: expires sessions that have not been active in a specified period of time.</li>
<li><strong>Validatable</strong>: provides validations of email and password. It's optional and can be customized, so you're able to define your own validations.</li>
<li><strong>Lockable</strong>: locks an account after a specified number of failed sign-in attempts. Can unlock via email or after a specified time period.</li>
</ul>
<p>See the <a href="https://github.com/plataformatec/devise#getting-started">devise documentation</a> on how to set it up.</p></div>
<div class='slide'>
<p class='slide_break_block'><a class='slide_break' id='slide-22' href='slides_rails_authentication.html#/22'>◻</a></p>
<p>When set up correctly devise gives you helper methods to use in your controllers and views:</p>
<ul>
<li><code>current_user</code></li>
<li><code>user_signed_in?</code> # to check if a user is signed in (in views and controllers)</li>
<li><code>before_action :authenticate_user!</code> # to make a controller only accessible to authenticated users</li>
</ul>
<p>It also adds new routes to your app:</p><div class="interstitial code">
<pre><code class="highlight plaintext"> Prefix Verb URI Pattern Controller#Action
new_user_session GET /users/sign_in devise/sessions#new
user_session POST /users/sign_in devise/sessions#create
destroy_user_session DELETE /users/sign_out devise/sessions#destroy
new_user_password GET /users/password/new devise/passwords#new
edit_user_password GET /users/password/edit devise/passwords#edit
user_password PATCH /users/password devise/passwords#update
PUT /users/password devise/passwords#update
POST /users/password devise/passwords#create
cancel_user_registration GET /users/cancel devise/registrations#cancel
new_user_registration GET /users/sign_up devise/registrations#new
edit_user_registration GET /users/edit devise/registrations#edit
user_registration PATCH /users devise/registrations#update
PUT /users devise/registrations#update
DELETE /users devise/registrations#destroy
POST /users devise/registrations#create
new_user_confirmation GET /users/confirmation/new devise/confirmations#new
user_confirmation GET /users/confirmation devise/confirmations#show
POST /users/confirmation devise/confirmations#create
</code></pre>
<button class="clipboard-button" data-clipboard-text=" Prefix Verb URI Pattern Controller#Action
new_user_session GET /users/sign_in devise/sessions#new
user_session POST /users/sign_in devise/sessions#create
destroy_user_session DELETE /users/sign_out devise/sessions#destroy
new_user_password GET /users/password/new devise/passwords#new
edit_user_password GET /users/password/edit devise/passwords#edit
user_password PATCH /users/password devise/passwords#update
PUT /users/password devise/passwords#update
POST /users/password devise/passwords#create
cancel_user_registration GET /users/cancel devise/registrations#cancel
new_user_registration GET /users/sign_up devise/registrations#new
edit_user_registration GET /users/edit devise/registrations#edit
user_registration PATCH /users devise/registrations#update
PUT /users devise/registrations#update
DELETE /users devise/registrations#destroy
POST /users devise/registrations#create
new_user_confirmation GET /users/confirmation/new devise/confirmations#new
user_confirmation GET /users/confirmation devise/confirmations#show
POST /users/confirmation devise/confirmations#create
">Copy</button>
</div>
<p>You will propably want to link to <code>new_user_session_path</code> for login,
<code>destroy_user_session</code> for logout and <code>new_user_registration</code> for registering
a new user.</p></div>
<div class='slide'>
<p class='slide_break_block'><a class='slide_break' id='slide-23' href='slides_rails_authentication.html#/23'>◻</a></p>
<h2 id="oauth-and-openid-connect"><a class="anchorlink" href="#oauth-and-openid-connect"><span>5</span> OAuth and OpenID Connect</a></h2><p>In many scenarios it might be more convenient for your users
to not have to register on your site, but to use another service
to authenticate. That way they don't have to remember another password.
And you might not have to handle passwords at all.</p><p>You can achieve this with Open Authentication,
or <a href="https://en.wikipedia.org/wiki/OAuth">OAuth</a> for short. It is a
standard for requesting Authentication and Authorization from
a priovider. If you only use OAuth for Authentication, not Authorization, then
you can use <a href="https://openid.net/connect/">OpenID Connect</a> on top of OAuth.</p><p>OpenID Connect can also be used with <a href="https://www.id-austria.gv.at/de/developer/anbinden/anbindung-mit-openid-connect">ID Austria</a>.</p></div>
<div class='slide'>
<p class='slide_break_block'><a class='slide_break' id='slide-24' href='slides_rails_authentication.html#/24'>◻</a></p>
<h3 id="rails-and-oauth"><a class="anchorlink" href="#rails-and-oauth"><span>5.1</span> Rails and OAuth</a></h3><p>The gem <code>omniauth</code> helps you deal with OAuth2, OpenID, LDAP, and many
other authentication providers.
The <a href="https://github.com/intridea/omniauth/wiki/List-of-Strategies">list of strategies</a>
is quite impressive. Think carefully about what services your users
are using, and which services might be useful to your app: could
you use Dropbox to authenticate, and also to deliver data directly
to your user's dropbox? Would it make sense to use Facebook or Twitter and also
send out messages that way? Or are your users very privacy conscious and
want to avoid Facebook and Google?</p></div>
<div class='slide'>
<p class='slide_break_block'><a class='slide_break' id='slide-25' href='slides_rails_authentication.html#/25'>◻</a></p>
<h3 id="providers"><a class="anchorlink" href="#providers"><span>5.2</span> Providers</a></h3><p>You will need the Gem <code>omniauth</code> and
additional gems for each provider. For example if you
want to use both Github and Stackoverflow for your web app geared
towards developers, you would need three gems:</p><div class="interstitial code">
<pre><code class="highlight plaintext">gem 'omniauth'
gem 'omniauth-github'
gem 'omniauth-stackoverflow'
</code></pre>
<button class="clipboard-button" data-clipboard-text="gem 'omniauth'
gem 'omniauth-github'
gem 'omniauth-stackoverflow'
">Copy</button>
</div>
</div>
<div class='slide'>
<p class='slide_break_block'><a class='slide_break' id='slide-26' href='slides_rails_authentication.html#/26'>◻</a></p>
<p>You need to register your app with the authentication
provider, eg. at <a href="https://developers.facebook.com/apps/">https://developers.facebook.com/apps/</a>
or <a href="https://apps.twitter.com/">https://apps.twitter.com/</a>.
You have to specify the URL of your web app, and a callback URL:</p><p><img src="images/oauth-app-config.png" alt="oauth app configuration"></p><p>There might also be a review process involved which might take
a few business days to go through.</p></div>
<div class='slide'>
<p class='slide_break_block'><a class='slide_break' id='slide-27' href='slides_rails_authentication.html#/27'>◻</a></p>
<p>You get back two pieces of information: a key and a secret.
In Twitter this looks like this:</p><p><img src="images/oauth-app-secret.png" alt="facebook app configuration"></p><p>(A word of warning: if you change the configuration in <code>developers.facebook.com</code> then
you will get a new key and secret!)</p><p>You need to add the key and the secret to the configuration of omniauth:</p><div class="interstitial code">
<pre><code class="highlight ruby"><span class="c1"># config/initializers/omniauth.rb:</span>
<span class="no">Rails</span><span class="p">.</span><span class="nf">application</span><span class="p">.</span><span class="nf">config</span><span class="p">.</span><span class="nf">middleware</span><span class="p">.</span><span class="nf">use</span> <span class="no">OmniAuth</span><span class="o">::</span><span class="no">Builder</span> <span class="k">do</span>
<span class="n">provider</span> <span class="ss">:twitter</span><span class="p">,</span> <span class="s1">'TWITTER_KEY'</span><span class="p">,</span> <span class="s1">'TWITTER_SECRET'</span>
<span class="k">end</span>
</code></pre>
<button class="clipboard-button" data-clipboard-text="
Rails.application.config.middleware.use OmniAuth::Builder do
provider :twitter, 'TWITTER_KEY', 'TWITTER_SECRET'
end
">Copy</button>
</div>
</div>
<div class='slide'>
<p class='slide_break_block'><a class='slide_break' id='slide-28' href='slides_rails_authentication.html#/28'>◻</a></p>
<p>If you plan on publishing your source code
you might want to set these values in a way that is NOT saved to the repository.
You could use environment variables for that:</p><div class="interstitial code">
<pre><code class="highlight ruby"><span class="c1"># config/initializers/omniauth.rb:</span>
<span class="no">Rails</span><span class="p">.</span><span class="nf">application</span><span class="p">.</span><span class="nf">config</span><span class="p">.</span><span class="nf">middleware</span><span class="p">.</span><span class="nf">use</span> <span class="no">OmniAuth</span><span class="o">::</span><span class="no">Builder</span> <span class="k">do</span>
<span class="n">provider</span> <span class="ss">:twitter</span><span class="p">,</span> <span class="no">ENV</span><span class="p">[</span><span class="s1">'TWITTER_KEY'</span><span class="p">],</span> <span class="no">ENV</span><span class="p">[</span><span class="s1">'TWITTER_SECRET'</span><span class="p">]</span>
<span class="k">end</span>
</code></pre>
<button class="clipboard-button" data-clipboard-text="
Rails.application.config.middleware.use OmniAuth::Builder do
provider :twitter, ENV['TWITTER_KEY'], ENV['TWITTER_SECRET']
end
">Copy</button>
</div>
<p>Then you can set the environment variables locally on the command line:</p><div class="interstitial code">
<pre><code class="highlight sh"><span class="nv">TWITTER_KEY</span><span class="o">=</span>abc
<span class="nv">TWITTER_SECRET</span><span class="o">=</span>123
</code></pre>
<button class="clipboard-button" data-clipboard-text="TWITTER_KEY=abc
TWITTER_SECRET=123
">Copy</button>
</div>
<p>If you deploy to heroku or dokku, use the command line interface to set
the variables there:</p><div class="interstitial code">
<pre><code class="highlight sh">heroku config:set <span class="nv">TWITTER_KEY</span><span class="o">=</span>abc
heroku config:set <span class="nv">TWITTER_SECRET</span><span class="o">=</span>123
dokku config:set <span class="nv">TWITTER_KEY</span><span class="o">=</span>abc
dokku config:set <span class="nv">TWITTER_SECRET</span><span class="o">=</span>123
</code></pre>
<button class="clipboard-button" data-clipboard-text="heroku config:set TWITTER_KEY=abc
heroku config:set TWITTER_SECRET=123
dokku config:set TWITTER_KEY=abc
dokku config:set TWITTER_SECRET=123
">Copy</button>
</div>
</div>
<div class='slide'>
<p class='slide_break_block'><a class='slide_break' id='slide-29' href='slides_rails_authentication.html#/29'>◻</a></p>
<h3 id="models"><a class="anchorlink" href="#models"><span>5.3</span> Models</a></h3><p>For authentication you need to save at least the provider name and the uid in your database
somewhere. In the simplest case you just save them in a user model:</p><div class="interstitial code">
<pre><code class="highlight shell"><span class="nb">rails </span>g model user provider uid
</code></pre>
<button class="clipboard-button" data-clipboard-text="rails g model user provider uid
">Copy</button>
</div>
<p>To use additional services and get additional info from the provider
you also need to save a per-user token and secret:</p><div class="interstitial code">
<pre><code class="highlight shell"><span class="nb">rails </span>g model user provider uid token secret
</code></pre>
<button class="clipboard-button" data-clipboard-text="rails g model user provider uid token secret
">Copy</button>
</div>
<p>If you want to enable that one user can log in via different
providers and still be recognised as the same user, you need to
create a user model with a has_many relationship to an authentiation model
that stores provider and uid.</p><p>But we will stick to the simple version:</p><div class="interstitial code">
<pre><code class="highlight ruby"><span class="k">class</span> <span class="nc">CreateUsers</span> <span class="o"><</span> <span class="no">ActiveRecord</span><span class="o">::</span><span class="no">Migration</span><span class="p">[</span><span class="mf">5.0</span><span class="p">]</span>
<span class="k">def</span> <span class="nf">change</span>
<span class="n">create_table</span> <span class="ss">:users</span> <span class="k">do</span> <span class="o">|</span><span class="n">t</span><span class="o">|</span>
<span class="n">t</span><span class="p">.</span><span class="nf">string</span> <span class="ss">:provider</span>
<span class="n">t</span><span class="p">.</span><span class="nf">string</span> <span class="ss">:uid</span>
<span class="n">t</span><span class="p">.</span><span class="nf">timestamps</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre>
<button class="clipboard-button" data-clipboard-text="class CreateUsers < ActiveRecord::Migration[5.0]
def change
create_table :users do |t|
t.string :provider
t.string :uid
t.timestamps
end
end
end
">Copy</button>
</div>
</div>
<div class='slide'>
<p class='slide_break_block'><a class='slide_break' id='slide-30' href='slides_rails_authentication.html#/30'>◻</a></p>
<h3 id="login-and-logout"><a class="anchorlink" href="#login-and-logout"><span>5.4</span> Login and Logout</a></h3><p>Omniauth is a "Rack Middleware". That means it is somewhat independent
of the Rails app you are building. It has access to the HTTP request, will
analyze that, and pass on data to your Rails app through the
environment variable <code>omniauth.auth</code>.</p><p>To log in you send the user to <code>/auth/:provider</code> (e.g. <code>/auth/facebook</code>).</p><div class="interstitial code">
<pre><code class="highlight ruby"><span class="o"><!--</span> <span class="n">app</span><span class="o">/</span><span class="n">views</span><span class="o">/</span><span class="n">layouts</span><span class="o">/</span><span class="n">application</span><span class="p">.</span><span class="nf">html</span><span class="p">.</span><span class="nf">erb</span> <span class="o">--></span>
<span class="o"><</span><span class="sx">% if </span><span class="n">current_user</span> <span class="sx">%>
Logged in as <%= current_user.name %></span>
<span class="o"><</span><span class="sx">%= link_to "log out", logout_path %>
<% else %>
log in with <%=</span> <span class="n">button_to</span> <span class="s2">"twitter"</span><span class="p">,</span> <span class="s2">"/auth/twitter"</span><span class="p">,</span> <span class="ss">method: :post</span><span class="p">,</span> <span class="ss">data: </span><span class="p">{</span><span class="ss">turbo: </span><span class="s2">"false"</span><span class="p">}</span> <span class="o">%></span>
<span class="o"><</span><span class="sx">% end </span><span class="o">%></span>
</code></pre>
<button class="clipboard-button" data-clipboard-text="<!-- app/views/layouts/application.html.erb -->
<% if current_user %>
Logged in as <%= current_user.name %>
<%= link_to "log out", logout_path %>
<% else %>
log in with <%= button_to "twitter", "/auth/twitter", method: :post, data: {turbo: "false"} %>
<% end %>
">Copy</button>
</div>
</div>
<div class='slide'>
<p class='slide_break_block'><a class='slide_break' id='slide-31' href='slides_rails_authentication.html#/31'>◻</a></p>
<p>This URL is handled by omniauth, not by your Rails app. Omniauth will send
the user's browser on to a URL at the provider. There the user can log in. After
that the browser is redirected to your app again, to <code>/auth/:provider/callback</code></p><p>This URL you need to map to a session controller:</p><div class="interstitial code">
<pre><code class="highlight ruby"><span class="c1"># config/routes.rb:</span>
<span class="n">match</span> <span class="s1">'/auth/:provider/callback'</span><span class="p">,</span> <span class="ss">to: </span><span class="s1">'sessions#create'</span><span class="p">,</span> <span class="ss">via: </span><span class="p">[</span><span class="ss">:get</span><span class="p">,</span> <span class="ss">:post</span><span class="p">]</span>
<span class="n">match</span> <span class="s1">'/auth/failure'</span><span class="p">,</span> <span class="ss">to: </span><span class="s1">'sessions#failure'</span><span class="p">,</span> <span class="ss">via: </span><span class="p">[</span><span class="ss">:get</span><span class="p">,</span> <span class="ss">:post</span><span class="p">]</span>
</code></pre>
<button class="clipboard-button" data-clipboard-text="match '/auth/:provider/callback', to: 'sessions#create', via: [:get, :post]
match '/auth/failure', to: 'sessions#failure', via: [:get, :post]
">Copy</button>
</div>
<p>In the session controller you can now read the data that omniauth provides
from the environment variable.</p></div>
<div class='slide'>
<p class='slide_break_block'><a class='slide_break' id='slide-32' href='slides_rails_authentication.html#/32'>◻</a></p>
<p>As a first step you could just print it out,
to see what data is provided:</p><div class="interstitial code">
<pre><code class="highlight ruby"><span class="k">def</span> <span class="nf">create</span>
<span class="n">render</span> <span class="ss">text: </span><span class="s2">"<pre>"</span> <span class="o">+</span> <span class="n">env</span><span class="p">[</span><span class="s2">"omniauth.auth"</span><span class="p">].</span><span class="nf">to_yaml</span> <span class="ow">and</span> <span class="k">return</span>
<span class="k">end</span>
</code></pre>
<button class="clipboard-button" data-clipboard-text="def create
render text: "<pre>" + env["omniauth.auth"].to_yaml and return
end
">Copy</button>
</div>
<p>The data always contains values for <code>provider</code> and <code>uid</code> at the
top level. There may be a lot more data.</p></div>
<div class='slide'>
<p class='slide_break_block'><a class='slide_break' id='slide-33' href='slides_rails_authentication.html#/33'>◻</a></p>
<p>Here some example data from a twitter login:</p><div class="interstitial code">
<pre><code class="highlight plaintext">provider: twitter
uid: '8506142'
info:
nickname: bjelline
name: Brigitte Jellinek
...
</code></pre>
<button class="clipboard-button" data-clipboard-text="provider: twitter
uid: '8506142'
info:
nickname: bjelline
name: Brigitte Jellinek
...
">Copy</button>
</div>
</div>
<div class='slide'>
<p class='slide_break_block'><a class='slide_break' id='slide-34' href='slides_rails_authentication.html#/34'>◻</a></p>
<p>Now let's look at <code>session#create</code>:
There are two basic cases to consider: either the user has logged in using
this authorisation method before (then we should find them in our database),
or they are logging in for the first time.</p><p>This can get quite involved, so we hide it away inside the user model:</p><div class="interstitial code">
<pre><code class="highlight ruby"><span class="k">def</span> <span class="nf">create</span>
<span class="n">user</span> <span class="o">=</span> <span class="no">User</span><span class="p">.</span><span class="nf">find_or_create_with_omniauth</span><span class="p">(</span><span class="n">request</span><span class="p">.</span><span class="nf">env</span><span class="p">[</span><span class="s1">'omniauth.auth'</span><span class="p">])</span>
<span class="k">if</span> <span class="n">user</span>
<span class="n">session</span><span class="p">[</span><span class="ss">:user_id</span><span class="p">]</span> <span class="o">=</span> <span class="n">user</span><span class="p">.</span><span class="nf">id</span>
<span class="n">redirect_to</span> <span class="n">root_path</span><span class="p">,</span> <span class="ss">notice: </span><span class="s1">'Logged in'</span>
<span class="k">else</span>
<span class="n">redirect_to</span> <span class="n">login_path</span><span class="p">,</span> <span class="ss">alert: </span><span class="s1">'Log in failed'</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre>
<button class="clipboard-button" data-clipboard-text="def create
user = User.find_or_create_with_omniauth(request.env['omniauth.auth'])
if user
session[:user_id] = user.id
redirect_to root_path, notice: 'Logged in'
else
redirect_to login_path, alert: 'Log in failed'
end
end
">Copy</button>
</div>
</div>
<div class='slide'>
<p class='slide_break_block'><a class='slide_break' id='slide-35' href='slides_rails_authentication.html#/35'>◻</a></p>
<p>In the model we pick apart the information from omniauth:</p><div class="interstitial code">
<pre><code class="highlight ruby"><span class="c1"># app/model/user.rb</span>
<span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">find_or_create_with_omniauth</span><span class="p">(</span><span class="n">auth</span><span class="p">)</span>
<span class="c1"># look for an existing authorisation</span>
<span class="c1"># provider + uid uniquely identify a user</span>
<span class="no">User</span><span class="p">.</span><span class="nf">find_or_create_by!</span><span class="p">(</span>
<span class="ss">provider: </span><span class="n">auth</span><span class="p">[</span><span class="s1">'provider'</span><span class="p">],</span>
<span class="ss">uid: </span><span class="n">auth</span><span class="p">[</span><span class="s1">'uid'</span><span class="p">]</span>
<span class="p">)</span>
<span class="k">end</span>
</code></pre>
<button class="clipboard-button" data-clipboard-text="
def self.find_or_create_with_omniauth(auth)
# look for an existing authorisation
# provider + uid uniquely identify a user
User.find_or_create_by!(
provider: auth['provider'],
uid: auth['uid']
)
end
">Copy</button>
</div>
<p>The ActiveRecord method <code>find_or_create_by</code> handles both cases in one: either it
finds an existing user or it creates a new one.</p></div>
<div class='slide'>
<p class='slide_break_block'><a class='slide_break' id='slide-36' href='slides_rails_authentication.html#/36'>◻</a></p>
<p>We don't really have a name for each user, but
we can fake that in the model:</p><div class="interstitial code">