Rails Tutorial DHBW-Heidenheim

Authentisierung

Bei der Authentisierung geht es darum, den Zugriff auf Ressourcen zu kontrollieren und ggf. die Ressourcen vor unbefugter Nutzung zu schützen. Ressourcen in einer Webanwendung können etwa Benutzerdaten wie Anschriften oder Bestellungen, geschützte Inhalte, die nur eine bestimmte Gruppe sehen darf oder administrative Einstellungen sein.

Im einfachsten Fall erfolgt die Authentisierung eines Benutzers durch Eingabe einer Kennung (meist Benutzername und Passwort). Stimmen diese mit den hinterlegten Daten überein, beginnt die Authorisierung, bei der der Benutzer die Rechte erhält, um etwas tun zu können. In komplexen Systemen, etwa in einem ERP-System, kann einem Benutzer durch Rollenvergabe und daran geknüpfte Berechtigungen Zugriff auf nur ganz bestimmte Teile des System gewährt werden. Eine sehr feingranulare Rechtevergabe ist so möglich. Die Komplexität des Authentisierungs- und Authorisierungssystems leitet sich von den Sicherheitsanforderungen der zu schützenden Ressourcen ab.

Techniken

In der Regel fordert ein Client eine Webseite über das HTTP-Protokoll vom entsprechenden Server an. Diese wird dann vom Browser des Clients interpretiert. Allerdings gibt es zwischen dem (Web)server und dem Client keine dauerhafte Verbindung. HTTP wird deshalb auch als zustandsloses Protokoll bezeichnet. Der (Web)server kennt zwar die IP-Adresse des Clients, diese ist aber keine eindeutige Identifikationsmöglichkeit für einen expliziten Client. Beispiel:

Client 1 fordert von einem Webserver eine Seite an. Üblicherwiese hat nicht jeder Client eine öffentliche IP-Adresse, sondern eine private, die von einem NAT-Router umgesetzt wird. Nach aussen hin wird mit der öffentlichen IP-Adresse des Routers auf den Webserver zugegriffen, wenn die Antwort kommt, weiss der Router durch das NAT-Verfahren, an welchen Client diese zu senden ist.

Der Client, von dem die Anfrage tatsächlich kam, lässt sich vom Webserver allein aufgrund der IP-Adresse nicht eindeutig identifizieren. Beide Clients treten nach aussen mit der selben öffentlichen IP-Adressen auf. Im schlimmsten Fall hätte einer der beiden Clients Zugriff auf eine Ressource z.B. eine Seite mit Bankdaten des anderen Clients.

Eine Lösung des Problems sind Sessions (Sitzungen) zwischen Client und Server. Dabei überträgt der Client bei jeder Anfrage auf eine Ressource des Webservers eine Sitzungsnummer bzw. Session ID. Hierdurch kann ein Client eindeutig identifiziert werden. Einer Session können auch, im Client oder beim Server, Daten zugeordnet werden. So wäre es beispielsweise möglich, in einem Online Shop Artikel in den Warenkorb zu legen, ohne am System angemeldet zu sein. Weitere Informationen zur Funktionsweise von Sessions finden sich hier .

HTTP Authentisierung

Die HTTP Authentisierung ist eine Möglichkeit, wie sich ein Client am Webserver anmelden kann. Wird eine Ressource abgerufen, die geschützt ist, antwortet der Webserver mit dem HTTP Statuscode 401 Unauthorized und dem Header WWW-Authenticate. Es öffnet sich ein Fenster, in dem Benutzername und Passwort eingegeben werden können. Diese werden dann vom Browser bei weiteren Anfragen an diesen Webserver mitgeschickt.

Diese Authentisierungsmethode ist sehr schnell einsetzbar. Für eine grössere Webanwendung ist die HTTP Authentisierung jedoch zu unflexibel.

Authentisierung auf Anwendungsebene

Der Client meldet sich innerhalb der Webapplication an. Wann und ob er authentisiert ist, entscheidet dabei die Anwendung selbst. Typischerweise werden Benutzerinformationen wie etwa der Benutzername und das Passwort in einer Datenbanktabelle gespeichert und mit den übertragenen Kennungsdaten vom Client verglichen. Stimmt dieser Vergleich überein, ist der Benutzer identifizert.

Die erfolgreiche Authentisierung kann entweder auf Datenbankebene der Webapplikation gespeichert werden, also etwa in einer Tabelle mit Session ID und Status angemeldet, oder eben in der Session selbst.

Anmerkung: Die Daten der Session sind nur bedingt sicher, da sie durch dritte manipuliert werden können. Daher sollten nur öffentliche Informationen in der Session zwischengespeichert werden. Auch die zu übertragenden Daten vom Client auf den Server und zurück sind nicht sicher. Sie können manipuliert werden, so können Sessions etwa gekapert oder es können fremde Sessions untergeschoben werden. Findet die Kommunikation zwischen Server und Client unverschlüsselt statt, sind diese Probleme nur schwer lösbar.

VPNs und SSL-Tunnel

Die Daten einer Sitzung zwischen Client und Webserver sind in der Regel unverschlüsselt. Sie können dadurch ausgelesen und auch manipuliert werden. Ein Angreifer, der die in Klartext übertragenen Daten ausgelesen hat, kann sich ungehindert am System Authentisieren etc.

Abhilfe schafft ein SSL-Tunnel . Es wird eine gesicherte und verschlüsselte Verbindung zwischen Client und Webserver aufgebaut. Die Daten werden sodann verschlüsselt übertragen. Ein Angreifer kann zwar die verschlüsselten Daten auslesen, muss diese jedoch erst entschlüsseln bevor er sich am System anmelden kann. Je nach dem, wie stark die Verschlüsselung gewählt wurde, kann es Jahre dauern, bis die Daten entschlüsselt sind.

Authentisierung am Beispiel

Für unser Beispiel führen wir die Authentisierung in der Anwendung durch. Die Authorisierung ist hier einfach, jeder Benutzer darf alle anderen sehen, aber nur sein eigenes Profil bearbeiten.

Als Kennung verwenden wir den Nickname und die Emailadresse. In einer realen Anwendung würde man vermutlich ein verschlüsselt zu speicherndes Kennwort verwenden.

Dazu müssen wir unsere Anwendung erweitern: Im Model muss nichts geändert werden, wir verwenden bereits vorhandene Attribute. Es wird lediglich eine Methode benötigt, die das Passwort aus der Datenbank mit dem übertragenen Passwort vergleicht (wird dies in einer Methode gekapselt, können wir später leicht ein anderes, "echtes" Kennwort verwenden). Wir brauchen allerdings einen neuen View, um die Benutzerdaten eingeben zu können.

Der Ablauf beim Login sieht dann so aus:

Im Gegensatz zum Login ist das abmelden recht einfach: Es werden einfach alle Sessiondaten gelöscht.

Umsetzung

Zunächst wird eine neue View im Ordner /app/views/people/ mit Namen login.html.erb angelegt. Darin wird ein Formular für die Eingabe des Benutzernamens und des Passworts sowie ein Login-Button dargestellt.


		<%= form_tag do %>
		   <label for="name">Nickname:</label>
		   <%= text_field_tag :name, params[:name]%><br />

		   <label for="password">Kennwort:</label>
		   <%= password_field_tag :password, params[:password]%><br />
		<%= submit_tag "Login"%>
		<% end %>
		
	

Wir erweitern den PeopleController um die Methoden login und logout:


		def login
		    if (request.post? )
		      user = Person.authenticate(params[:name],params[:password])
		      if (user)
		        session[:user_id] = user.id
		        redirect_to(user)
		      else
		        params[:password] = ""
		        render :action => "login"
		      end
		    end
		  end
		
		def logout
		  reset_session
		  redirect_to("/")
		end

		
	

In der ersten Zeile wird geprüft, ob es sich um einen post-request handelt. Das ist im Allgemeinen der Fall, wenn die Methode aus einem Formular aufgerufen wurde, also der Benutzer auf den Loginbutton geklickt hat.

In der authenticate-Methode wird geprüft, ob die eingegebenen Daten korrekt sind. Ist dies der Fall, wird die Benutzer id in der Session als Parameter user_id gespeichert. Ansonsten wird erneut der Anmeldedialog angezeigt.

In der Methode logout werden durch den Befehl reset_session alle Sessiondaten gelöscht. Durch redirect_to("/") wird die Startseite der Webanwendung ausgegeben.

Das PersonModel wird um die Methode authenticate erweitert:


		def self.authenticate(name,password)
		    user = self.find_by_Nickname(name)
		    # Wir nehmen die email-Adresse als Kennwort
		    if (user.email != password)
		      user = nil
		    end
		    user
		  end
	

Wird ein passender Benutzer in der Datenbank gefunden, wird das entsprechende user-Objekt zurügegeben, ansonsten nil.

Im Grunde ist unser Authentisierungssystem jetzt einsatzbereit. Wenn wir die Loginmethode aufrufen, bekommen wir folgendes angezeigt:

Hat sich ein Benutzer angemeldet, ist in der Session der Parameter user_id gesetzt. Soll also etwa beim Editieren geprüft werden, ob dies zulässig ist, kann so die Identität des angemeldeten Benutzers geprüft werden.


			# GET /people/1/edit
			  def edit
			    if session[:user_id]
			       @person = Person.find(session[:user_id])
			    else
			       render :action=>"login"
			   end
			  end
			
Wenn session[:user_id] eine Benutzer ID enthält, ist ein Benutzer angemeldet. Ist dies der Fall, wird der entsprechende Datensatz gesucht und dann bearbeitet.

Geht das nicht auch schneller und besser?

Wir haben hier ein eigenes Authentisierungssystem entwickelt. Dieses System deckt nur die grundlegende Funktion ab. Wir haben auch kein ordentliches Passwort verwendet, keine email-Adresse verifiziert und so weiter. Es hat auch wenig Sinn, ein eigenes Authentisierungspaket zu entwickeln, es gibt bereits genügend. Ziel dieser Lesson war es, zu zeigen, wie die Authentisierung im Prinzip (und deshalb natürlich auch in den verfügbaren Paketen) funktioniert.

Authentisierungssysteme können als Plugins in die Webanwendung in wenigen Minuten eingebaut werden, verbreitete Lösungen sind etwa Authlogic , devise , restful authentication oder das im Screencast gezeigte Authentisierungsmodul der nifty generators .

Devise in die Beispielanwendung einbauen

Im Screencast wurde eine Authentisierungsmethode mit den "nifty generators" gezeigt. Um ein zweites Authentisierungssystem zu demonstrieren, wird Devise in die Beispielanwendung eingebaut.

Zunächst muss das entsprechende Paket installiert werden, am besten mit bundle


#/Gemfile
source 'http://rubygems.org'

gem 'rails', '3.0.3'
gem 'devise'
...
$ bundle install

Nach der Installation von Devise wird ein Generator ausgeführt, der alle notwendigen Models, Controllers und Views erzeugt:

 $ rails generate devise:install 

Der Generator erzeugt einen Ausgabe, die zwei wichtige Hinweise enthält:


===============================================================================

Some setup you must do manually if you haven't yet:

  1. Setup default url options for your specific environment. Here is an
     example of development environment:

       config.action_mailer.default_url_options = { :host => 'localhost:3000' }

     This is a required Rails configuration. In production it must be the
     actual host of your application

  2. Ensure you have defined root_url to *something* in your config/routes.rb.
     For example:

       root :to => "home#index"

===============================================================================

Für den Betrieb von Devise muss der ActionMailer mit dem korrekten Hostnamen versorgt werden:


#/config/environments/development.rb
Anwendung::Application.configure do
 ...
  #For Authentication - DEVISE
  config.action_mailer.default_url_options = { :host => 'localhost:3000' }
...
end

Wenn die Anwendung produktiv betrieben wird, muss die Konfiguration für den ActionMailer in der Datei /config/environments/production.rb mit der korrekten URL gesetzt werden.

Devise benötigt eine Root-Route. Diese wird verwendet, wenn ein Benutzer etwa eine Ressource anfordert, auf die er nicht zugreifen darf - er wird zur Startseite (Root-Route) umgeleitet.


#config/routes.rb
Anwendung::Application.routes.draw do
...
root :to => "landing#index" 
...
end

Wenn noch kein Users-Modell existiert, wird für Devise eines generiert:

$ rails generate devise User

Anschliessend wird die Datenbanktabelle erzeugt:

$ rake db:migrate

In der Datei /config/routes.rb wurde ein Eintrag erzeugt, worüber das gesamte Routing läuft:


#/config/routes.rb
Anwendung::Application.routes.draw do
...
devise_for :users
...
end

Zum jetzigen Entwicklungszeitpunkt sind die exakte Routen für Devise noch nicht bekannt. Der Befehl:


$ rake routes

erzeugt eine Ausgabe mit allen Routen des Projektes. Für Devise sind das etwa:


...
new_user_session 		GET    	/users/sign_in(.:format)               	{:controller=>"devise/sessions", :action=>"new"}
user_session 			POST   	/users/sign_in(.:format)               	{:controller=>"devise/sessions", :action=>"create"}
destroy_user_session 		GET    	/users/sign_out(.:format)              	{:controller=>"devise/sessions", :action=>"destroy"}
user_password 			POST 	/users/password(.:format)              	{:controller=>"devise/passwords", :action=>"create"}
new_user_password 		GET    	/users/password/new(.:format)          	{:controller=>"devise/passwords", :action=>"new"}
edit_user_password 		GET    	/users/password/edit(.:format)         	{:controller=>"devise/passwords", :action=>"edit"}
				PUT    /users/password(.:format)              	{:controller=>"devise/passwords", :action=>"update"}
cancel_user_registration 	GET    /users/cancel(.:format)               	{:controller=>"devise/registrations", :action=>"cancel"}
user_registration 		POST   /users(.:format)                       	{:controller=>"devise/registrations", :action=>"create"}
new_user_registration 		GET    /users/sign_up(.:format)               	{:controller=>"devise/registrations", :action=>"new"}
edit_user_registration 		GET    /users/edit(.:format)                  	{:controller=>"devise/registrations", :action=>"edit"}
				PUT    /users(.:format)                       	{:controller=>"devise/registrations", :action=>"update"}
				DELETE /users(.:format)                       	{:controller=>"devise/registrations", :action=>"destroy"}
...

Devise ist jetzt erfolgreich installiert und kann in der Webanwendung verwendet werden. Wie in der Ausgabe von rake routes zu sehen ist, kann ein Benutzer via http://localhost:3000/users/sign_up registriert werden:

Benutzer können sich via http://localhost:3000/users/sign_in einloggen:

Typischerweise gibt es in der Anwendung einen Link, mit dem sich ein Benutzer einloggen, registrieren oder ausloggen kann. Das Layout /app/views/layouts/application.html.erb wird erweitert:


<div id="benutzer_navigation">
  <% if user_signed_in? %>
    Eingeloggt als <%= current_user.email %>. Nicht Ihr Mitgliedsname?
    <%= link_to "Logout", destroy_user_session_path %>
  <% else %>
    <%= link_to "Registrieren", new_user_registration_path %> oder <%= link_to "einloggen", new_user_session_path %>
  <% end %>
</div>

Benutzer können sich jetzt zwar registrieren und auch einloggen, die nicht öffentlichen Bereiche der Web-Anwendung sind allerdings noch immer frei zugänglich. Das wird verhindert, indem im jeweiligen Controller eine before_filter-Methode eingefügt wird:


class GroupsController < ApplicationController
before_filter :authenticate_user!
...
end

Wenn eine Methode des GroupsControllers aufgerufen wird, wird zuerst die before_filter-Methode ausgeführt. Die Methode wurde mit installiert. Es wird geprüft, ob ein Benutzer eingeloggt ist. Wenn das nicht der Fall ist, wird der Benutzer auf die Startseite umgeleitet, ansonsten darf er zugreifen.

In manchen Fällen sind nur einzelne Methoden eines Controllers zu schützen, etwa bearbeiten:


class SomeController < ApplicationController
before_filter :authenticate_user!, :only=>[:edit]
...
end

In manchen Fälle sind nur einzelne Methoden öffentlich, etwa anzeigen:


class SomeController < ApplicationController
before_filter :authenticate_user!, :except=>[:show]
...
end

Wenn sich ein Benutzer registriert, können bis jetzt nur die Emailadresse und das Passwort erfasst werden. Ein Benutzer für die Beispielanwendung hat jedoch mehrere Attribute, die erfasst werden müssen - etwa Nickname oder Geburtsdatum. Die Views von Devise sind bisher nicht zugänglich, diese liegen in einem Library-Verzeichnis und müssen zunächst zugänglich gemacht machen:


rails generate devise:views

Die Devise-Views sind im Verzeichnis /app/views/devise zu finden.

Bevor die zusätzlichen Attribute des User-Modells erfasst werden können, muss dieses erweitert werden. Das kann über eine neue Migration gemacht werden, oder die bestehende Migration wird erweitert:


	class DeviseCreateUsers < ActiveRecord::Migration
	  def self.up
	    create_table(:users) do |t|
	      t.database_authenticatable :null => false
	      t.recoverable
	      t.rememberable
	      t.trackable
		#Zusätzliche User-Attribute
	      t.string :nickname
	      t.string :name
	      t.string :vname
	      t.date   :geburtsdatum
	      t.string :plz
	      t.string :geschlecht
	      t.string :ich
	      t.string :ichkurz
		#Zusätzliche User-Attribute - ENDE
	      # t.encryptable
	      # t.confirmable
	      # t.lockable :lock_strategy => :failed_attempts, :unlock_strategy => :both
	      # t.token_authenticatable
...
	end
	

Für die Erfassung der User-Attribute wird die View /app/views/devise/registrations/new.html.erb erweitert:


<h2>Sign up</h2>

<%= form_for(resource, :as => resource_name, :url => registration_path(resource_name)) do |f| %>
  <%= devise_error_messages! %>

  <p><%= f.label :nickname %><br />
  <%= f.text_field :nickname %></p>

  <p><%= f.label :name %><br />
  <%= f.text_field :name %></p>

  <p><%= f.label :vname %><br />
  <%= f.text_field :vname %></p>

  <p><%= f.label :email %><br />
  <%= f.email_field :email %></p>

  <p><%= f.label :geburtsdatum %><br />
  <%= f.date_select :geburtsdatum, :start_year=>1950 %></p>

  <p><%= f.label :plz %><br />
  <%= f.text_field :plz %></p>

  <p><%= f.label :geschlecht %><br />
  <%= f.select :geschlecht, ["male", "female"] %></p>

  <p><%= f.label :ich %><br />
  <%= f.text_field :ich %></p>

  <p><%= f.label :ichkurz %><br />
  <%= f.text_field :ichkurz %></p>

  <p><%= f.label :password %><br />
  <%= f.password_field :password %></p>

  <p><%= f.label :password_confirmation %><br />
  <%= f.password_field :password_confirmation %></p>

  <p><%= f.submit "Sign up" %></p>
<% end %>

<%= render :partial => "devise/shared/links" %>

Das sieht dann so aus: