Alternativecustom authentication HowTo » History » Version 11

Jean-Philippe Lang, 2014-10-04 09:51
fix file name (#18009)

1 1 Andrew R Jackson
h1. Alternative (custom) Authentication HowTo
2 1 Andrew R Jackson
3 9 Mischa The Evil
{{>toc}}
4 9 Mischa The Evil
5 1 Andrew R Jackson
h2. Intro
6 1 Andrew R Jackson
7 1 Andrew R Jackson
This page explains how to get Redmine to authenticate users against a different database. Perhaps you're running (and even wrote) an app that already stores account records, and you want Redmine to use those. Presumably that app doesn't support OpenID, else you'd be configuring that.
8 1 Andrew R Jackson
9 1 Andrew R Jackson
Having Redmine defer authentication to your other app is helpful to users--they only have to remember one set of passwords and the accounts can't get out-of-sync. And if they are registered with your main app, Redmine can be configured to automatically add them to its own table (without storing any password info) when they first log in there.
10 1 Andrew R Jackson
11 1 Andrew R Jackson
h2. Redmine Support For Alternative Authentication
12 1 Andrew R Jackson
13 1 Andrew R Jackson
Redmine has specific support for alternative/custom authentication which makes implementing it very easy.
14 1 Andrew R Jackson
* +@auth_sources@ table+
15 1 Andrew R Jackson
** You will add a record here specific to your custom authentication.
16 1 Andrew R Jackson
* +@AuthSource@ class+
17 1 Andrew R Jackson
** You will create your own subclass of this, and implement the @authenticate()@ method.
18 1 Andrew R Jackson
19 1 Andrew R Jackson
Redmine's authentication process is along these lines (LDAP & OpenID assumed to be disabled):
20 1 Andrew R Jackson
# First, try to authenticate @login@ & @password@ against Redmine's internal table (@users@).
21 1 Andrew R Jackson
# If that fails, try each alternative authentication source registered in the @auth_sources@ table, stopping when one of the sources succeeds.
22 1 Andrew R Jackson
# If that fails, reject the login attempt.
23 1 Andrew R Jackson
24 1 Andrew R Jackson
_Note: Redmine will make a note of which source successfully authenticated a specific user. That source will be tried first the next time that user tries to login. Administrators can manually set/override that on a user-by-user basis via @Administration -> Users -> {user} -> Authentication mode@._
25 1 Andrew R Jackson
26 1 Andrew R Jackson
h2. Implementing An Alternative Authentication Source
27 1 Andrew R Jackson
28 1 Andrew R Jackson
This article assumes the alternative authentication involves querying a table (or tables) in some other database. However, the approach can be generalized to authenticate against pretty much anything else (some secret file, some network service, whatever fun scenario you have); you may want to examine the Redmine code in @app/models/auth_source_ldap.rb@ for a non-database alternative authentication example.
29 1 Andrew R Jackson
30 1 Andrew R Jackson
h3. Insert a Sensible @auth_sources@ Record
31 1 Andrew R Jackson
32 1 Andrew R Jackson
First, we should decide what our @auth_sources@ record will look like. Redmine core and our @AuthSource@ subclass code will make use of that info, so it's good to figure this out up front.
33 1 Andrew R Jackson
34 10 Andrew R Jackson
For reference, here is the (Redmine 2.4.1) @auth_sources@ table:
35 1 Andrew R Jackson
36 1 Andrew R Jackson
<pre>
37 1 Andrew R Jackson
+-------------------+--------------+------+-----+---------+----------------+
38 1 Andrew R Jackson
| Field             | Type         | Null | Key | Default | Extra          |
39 1 Andrew R Jackson
+-------------------+--------------+------+-----+---------+----------------+
40 1 Andrew R Jackson
| id                | int(11)      | NO   | PRI | NULL    | auto_increment |
41 1 Andrew R Jackson
| type              | varchar(30)  | NO   |     |         |                |
42 1 Andrew R Jackson
| name              | varchar(60)  | NO   |     |         |                |
43 1 Andrew R Jackson
| host              | varchar(60)  | YES  |     | NULL    |                |
44 1 Andrew R Jackson
| port              | int(11)      | YES  |     | NULL    |                |
45 1 Andrew R Jackson
| account           | varchar(255) | YES  |     | NULL    |                |
46 10 Andrew R Jackson
| account_password  | varchar(255) | YES  |     |         |                |
47 1 Andrew R Jackson
| base_dn           | varchar(255) | YES  |     | NULL    |                |
48 1 Andrew R Jackson
| attr_login        | varchar(30)  | YES  |     | NULL    |                |
49 1 Andrew R Jackson
| attr_firstname    | varchar(30)  | YES  |     | NULL    |                |
50 1 Andrew R Jackson
| attr_lastname     | varchar(30)  | YES  |     | NULL    |                |
51 1 Andrew R Jackson
| attr_mail         | varchar(30)  | YES  |     | NULL    |                |
52 1 Andrew R Jackson
| onthefly_register | tinyint(1)   | NO   |     | 0       |                |
53 1 Andrew R Jackson
| tls               | tinyint(1)   | NO   |     | 0       |                |
54 10 Andrew R Jackson
| filter            | varchar(255) | YES  |     | NULL    |                |
55 10 Andrew R Jackson
| timeout           | int(11)      | YES  |     | NULL    |                |
56 1 Andrew R Jackson
+-------------------+--------------+------+-----+---------+----------------+
57 1 Andrew R Jackson
</pre>
58 1 Andrew R Jackson
59 10 Andrew R Jackson
This schema has been relatively stable, although older Redmine versions may be missing the last 2 columns (e.g. Redmine 0.9 doesn't have them).
60 10 Andrew R Jackson
61 1 Andrew R Jackson
How our @AuthSource@ subclass code will make use of these fields is more-or-less up to us, but there are two key constraints from Redmine:
62 1 Andrew R Jackson
# +@type@ must be the name of your @AuthSource@ subclass+
63 1 Andrew R Jackson
#* Redmine will use this field to instantiate your class and call its @authenticate()@ method when attempting to authenticate a login attempt using your custom source.
64 1 Andrew R Jackson
#* This class name should begin with @AuthSource@.
65 1 Andrew R Jackson
#* We'll put "@AuthSourceMyCustomApp@"
66 1 Andrew R Jackson
# +@onthefly_register has a 1 or 0@+
67 1 Andrew R Jackson
#* Redmine will use this field to determine if unknown users (logins Redmine doesn't know about yet) can be registered within Redmine using this authentication source. Otherwise, if you put "0" here, an Administrator will first have to register the user manually (and presumably set their @Authentication mode@)--Redmine won't add them automatically.
68 1 Andrew R Jackson
69 1 Andrew R Jackson
Here's how we'll use these fields (substitute your own values):
70 1 Andrew R Jackson
71 1 Andrew R Jackson
| *Field* | *Our Value* | *Comment* |
72 1 Andrew R Jackson
| id | @NULL@ | Let the database engine provide the id. |
73 1 Andrew R Jackson
| type | "AuthSourceMyCustomApp" | Name of your @AuthSource@ subclass |
74 1 Andrew R Jackson
| name | "MyCustomApp" | Name of this alternative authentication source. Will be displayed in Administration UI pages.|
75 1 Andrew R Jackson
| host | "myApp.other.host.edu" | Host name where the other database lives. This article doesn't assume that's the same host as where your Redmine database is. |
76 1 Andrew R Jackson
| port | 3306 | Port for the database on that other host. |
77 1 Andrew R Jackson
| account | "myDbUser" | Account name for accessing that other database. |
78 1 Andrew R Jackson
| account_password | "myDbPass" | Password for that account for accessing the other database. |
79 8 Olivier Pinette
| base_dn | "mysql:myApp" | This field sounds very LDAP-ish. Sorry. We will interpret it to mean "BASic Database Name data" and store within a string of the form "@{dbAdapterName}:{dbName}@".|
80 1 Andrew R Jackson
| attr_login | "name" | What field in your other database table contains the login? |
81 1 Andrew R Jackson
| attr_firstname | "firstName" | What field in your other database table contains the user's first name? |
82 1 Andrew R Jackson
| attr_lastname | "lastName" | What field in your other database table contains the user's last name? |
83 1 Andrew R Jackson
| attr_mail | "email" | What field in your other database table contains the user's email? |
84 1 Andrew R Jackson
| onthefly_register | 1 | Yes, if this source authenticates the user then Redmine should create an internal record for them (w/o password info). |
85 1 Andrew R Jackson
| tls | 0 | Dunno. 0 for "no". |
86 10 Andrew R Jackson
| filter | NULL | Dunno. NULL is the default though. |
87 10 Andrew R Jackson
| timeout | NULL | Dunno. Timeout while waiting for alternative authenticator? NULL is the default though. |
88 1 Andrew R Jackson
89 1 Andrew R Jackson
_Note: The @attr_*@ fields are not always needed. They are used by the LDAP authentication source to map LDAP attributes to Redmine attributes. I recommend using them, however, since they make the @authentication()@ code more widely applicable (fewer changes necessary for you to use the code in your specific situation)._
90 1 Andrew R Jackson
91 10 Andrew R Jackson
So we insert the record into our Redmine's (v2.4.1) @auth_sources@ table with SQL like the following:
92 1 Andrew R Jackson
93 1 Andrew R Jackson
<pre>
94 1 Andrew R Jackson
<code class="Sql">
95 10 Andrew R Jackson
INSERT INTO auth_sources VALUES
96 10 Andrew R Jackson
  (NULL, 'AuthSourceMyCustomApp', 'MyCustomApp', 'myApp.other.host.edu', 3306,
97 10 Andrew R Jackson
  'myDbUser', 'myDbPass', 'mysql:myApp', 'name', 'firstName', 'lastName', 'email',
98 10 Andrew R Jackson
  1, 0, null, null)
99 1 Andrew R Jackson
</code>
100 1 Andrew R Jackson
</pre>
101 1 Andrew R Jackson
102 1 Andrew R Jackson
h3. Implement Your @AuthSource@ Subclass
103 1 Andrew R Jackson
104 1 Andrew R Jackson
Create a new file for your @AuthSource@ subclass in @app/models/@, following the naming scheme of the existing @auth_source.rb@ and @auth_source_ldap.rb@.
105 11 Jean-Philippe Lang
* Here we'll use @app/models/auth_source_my_custom_app.rb@
106 1 Andrew R Jackson
107 1 Andrew R Jackson
Implement the class such that a call to its @authenticate()@ method will contact the other database and use the table there to check the provided login credentials. The values in your @auth_sources@ table record above are available via instances variables (e.g. @self.host@, @self.base_dn@, @self.attr_firstname@). 
108 1 Andrew R Jackson
109 1 Andrew R Jackson
Here's our commented class:
110 1 Andrew R Jackson
111 1 Andrew R Jackson
<pre>
112 1 Andrew R Jackson
<code class="ruby">
113 1 Andrew R Jackson
# Redmine MyApp Authentication Source
114 1 Andrew R Jackson
#
115 1 Andrew R Jackson
# Copyright (C) 2010 Andrew R Jackson
116 1 Andrew R Jackson
#
117 1 Andrew R Jackson
# This program is free software; you can redistribute it and/or
118 1 Andrew R Jackson
# modify it under the terms of the GNU General Public License
119 1 Andrew R Jackson
# as published by the Free Software Foundation; either version 2
120 1 Andrew R Jackson
# of the License, or (at your option) any later version.
121 1 Andrew R Jackson
#
122 1 Andrew R Jackson
# This program is distributed in the hope that it will be useful,
123 1 Andrew R Jackson
# but WITHOUT ANY WARRANTY; without even the implied warranty of
124 1 Andrew R Jackson
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
125 1 Andrew R Jackson
# GNU General Public License for more details.
126 1 Andrew R Jackson
#
127 1 Andrew R Jackson
# You should have received a copy of the GNU General Public License
128 1 Andrew R Jackson
# along with this program; if not, write to the Free Software
129 1 Andrew R Jackson
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
130 1 Andrew R Jackson
131 1 Andrew R Jackson
# Let's have a new class for our ActiveRecord-based connection
132 1 Andrew R Jackson
# to our alternative authentication database. Remember that we're
133 1 Andrew R Jackson
# not assuming that the alternative authentication database is on
134 1 Andrew R Jackson
# the same host (and/or port) as Redmine's database. So its current
135 1 Andrew R Jackson
# database connection may be of no use to us. ActiveRecord uses class
136 1 Andrew R Jackson
# variables to store state (yay) like current connections and such; thus,
137 1 Andrew R Jackson
# dedicated class...
138 1 Andrew R Jackson
class MyAppCustomDB_ActiveRecord < ActiveRecord::Base
139 1 Andrew R Jackson
  PAUSE_RETRIES = 5
140 1 Andrew R Jackson
  MAX_RETRIES = 50
141 1 Andrew R Jackson
end
142 1 Andrew R Jackson
143 1 Andrew R Jackson
# Subclass AuthSource
144 1 Andrew R Jackson
class AuthSourceMyCustomApp < AuthSource
145 1 Andrew R Jackson
146 1 Andrew R Jackson
  # authentication() implementation
147 1 Andrew R Jackson
  # - Redmine will call this method, passing the login and password entered
148 1 Andrew R Jackson
  #   on the Sign In form.
149 1 Andrew R Jackson
  #
150 1 Andrew R Jackson
  # +login+ : what user entered for their login
151 1 Andrew R Jackson
  # +password+ : what user entered for their password
152 1 Andrew R Jackson
  def authenticate(login, password)
153 1 Andrew R Jackson
    retVal = nil
154 1 Andrew R Jackson
    unless(login.blank? or password.blank?)
155 1 Andrew R Jackson
      # Get a connection to the authenticating database.
156 1 Andrew R Jackson
      # - Don't use ActiveRecord::Base when using establish_connection() to get at
157 1 Andrew R Jackson
      #   your alternative database (leave Redmine's current connection alone).
158 1 Andrew R Jackson
      #   Use class you prepped above.
159 1 Andrew R Jackson
      # - Recall that the values stored in the fields of your auth_sources
160 1 Andrew R Jackson
      #   record are available as self.fieldName
161 1 Andrew R Jackson
162 1 Andrew R Jackson
      # First, get the DB Adapter name and database to use for connecting:
163 1 Andrew R Jackson
      adapter, dbName = self.base_dn.split(':')
164 1 Andrew R Jackson
165 1 Andrew R Jackson
      # Second, try to get a connection, safely dealing with the MySQL<->ActiveRecord
166 1 Andrew R Jackson
      # failed connection bug that can still arise to this day (regardless of 
167 1 Andrew R Jackson
      # reconnect, oddly).
168 1 Andrew R Jackson
      retryCount = 0
169 1 Andrew R Jackson
      begin
170 1 Andrew R Jackson
        connPool = MyAppCustomDB_ActiveRecord.establish_connection(
171 1 Andrew R Jackson
          :adapter  => adapter,
172 1 Andrew R Jackson
          :host     => self.host,
173 1 Andrew R Jackson
          :port     => self.port,
174 1 Andrew R Jackson
          :username => self.account,
175 1 Andrew R Jackson
          :password => self.account_password,
176 1 Andrew R Jackson
          :database => dbName,
177 1 Andrew R Jackson
          :reconnect => true
178 1 Andrew R Jackson
        )
179 1 Andrew R Jackson
        db = connPool.checkout()
180 1 Andrew R Jackson
      rescue => err # for me, always due to dead connection; must retry bunch-o-times to get a good one if this happens
181 1 Andrew R Jackson
        if(retryCount < MyAppCustomDB_ActiveRecord::MAX_RETRIES)
182 1 Andrew R Jackson
          sleep(1) if(retryCount < MyAppCustomDB_ActiveRecord::PAUSE_RETRIES)
183 1 Andrew R Jackson
          retryCount += 1
184 1 Andrew R Jackson
          connPool.disconnect!
185 1 Andrew R Jackson
          retry # start again at begin
186 1 Andrew R Jackson
        else # too many retries, serious, reraise error and let it fall through as it normally would in Rails.
187 1 Andrew R Jackson
          raise
188 1 Andrew R Jackson
        end
189 1 Andrew R Jackson
      end
190 1 Andrew R Jackson
191 1 Andrew R Jackson
      # Third, query the alternative authentication database for needed info. SQL
192 1 Andrew R Jackson
      # sufficient, obvious, and doesn't require other setup/LoC. Even more the
193 1 Andrew R Jackson
      # case if we have our database engine compute our digests (here, the whole
194 1 Andrew R Jackson
      # username is a salt). SQL also nice if your alt auth database doesn't have
195 1 Andrew R Jackson
      # AR classes and is not part of a Rails app, etc.
196 1 Andrew R Jackson
      resultRow = db.select_one(
197 1 Andrew R Jackson
        "SELECT #{self.attr_login}, #{self.attr_firstname}, #{self.attr_lastname}, #{self.attr_mail} " +
198 1 Andrew R Jackson
        "FROM genboreeuser " +
199 1 Andrew R Jackson
        "WHERE SHA1(CONCAT(#{self.attr_login}, password)) = SHA1(CONCAT('#{db.quote_string(login)}', '#{db.quote_string(password)}'))"
200 1 Andrew R Jackson
      )
201 1 Andrew R Jackson
202 1 Andrew R Jackson
      unless(resultRow.nil? or resultRow.empty?)
203 1 Andrew R Jackson
        user = resultRow[self.attr_login]
204 1 Andrew R Jackson
        unless(user.nil? or user.empty?)
205 1 Andrew R Jackson
          # Found a record whose login & password digest matches that computed
206 1 Andrew R Jackson
          # from Sign Inform parameters. If allowing Redmine to automatically
207 1 Andrew R Jackson
          # register such accounts in its internal table, return account
208 1 Andrew R Jackson
          # information to Redmine based on record found.
209 1 Andrew R Jackson
          retVal =
210 6 Anibal Sanchez
          {
211 1 Andrew R Jackson
            :firstname => resultRow[self.attr_firstname],
212 1 Andrew R Jackson
            :lastname => resultRow[self.attr_lastname],
213 1 Andrew R Jackson
            :mail => resultRow[self.attr_mail],
214 1 Andrew R Jackson
            :auth_source_id => self.id
215 6 Anibal Sanchez
          } if(onthefly_register?)
216 1 Andrew R Jackson
        end
217 1 Andrew R Jackson
      end
218 1 Andrew R Jackson
    end
219 1 Andrew R Jackson
    # Check connection back into pool.
220 1 Andrew R Jackson
    connPool.checkin(db)
221 1 Andrew R Jackson
    return retVal
222 1 Andrew R Jackson
  end
223 1 Andrew R Jackson
224 1 Andrew R Jackson
  def auth_method_name
225 1 Andrew R Jackson
    "MyCustomApp"
226 1 Andrew R Jackson
  end
227 1 Andrew R Jackson
end
228 1 Andrew R Jackson
229 1 Andrew R Jackson
</code>
230 1 Andrew R Jackson
</pre>
231 1 Andrew R Jackson
232 1 Andrew R Jackson
h2. Deploy & Test
233 1 Andrew R Jackson
234 1 Andrew R Jackson
Save your new class in @app/model/@ and restart Redmine.
235 1 Andrew R Jackson
236 1 Andrew R Jackson
* You ought to be able to try to log in as a use that exists in your alternative database but that doesn't exist in your Redmine instance.
237 1 Andrew R Jackson
* Existing Redmine accounts (and passwords) should continue to work.
238 1 Andrew R Jackson
* If you examine Redmine's @users@ table, you should see records appear after each successful login that used your alternative database.
239 1 Andrew R Jackson
** @hashed_password@ will be empty for those records.
240 1 Andrew R Jackson
** @auth_source_id@ will have the @id@ from @auth_sources@ which worked to authenticate the user; @NULL@ means "use internal Redmine authentication". The Administrator can also manually set this value via the @Authentication mode@ UI widget I mentioned above.
241 1 Andrew R Jackson
* Users authenticated with an alternative source will not be able to change their passwords using Redmine (great check by the core code) and will see an error message if they try to do so.
242 1 Andrew R Jackson
243 1 Andrew R Jackson
h2. Follow-Up
244 1 Andrew R Jackson
245 1 Andrew R Jackson
If you want to ONLY use your alternative authentication source for Redmine Sign In, remove the "Register" button. We did this by removing the @menu.push :register@ line in @lib/redmine.rb@. And we turned off the "Lost Password" feature via @Administration -> Settings -> Authentication -> Lost password@.
246 1 Andrew R Jackson
247 4 Paco Alcaide
<pre>
248 1 Andrew R Jackson
This was all pretty fast and simple to set up, thanks to how Redmine is organized and that some thought about permitting this kind of thing had been made. Quality. I hope I didn't get anything too wrong.
249 1 Andrew R Jackson
</pre>
250 1 Andrew R Jackson
251 6 Anibal Sanchez
h2. Joomla
252 6 Anibal Sanchez
253 7 Glen Vanderhel
We've customized the code to integrate Redmine with Joomla. Please, check the attachment to review an implementation for Joomla 2.5 and Redmine 2.0.3.
254 6 Anibal Sanchez
255 6 Anibal Sanchez
As Joomla does not have lastname in the users table, we have added a Html tag to show an icon in Redmine.
256 6 Anibal Sanchez
257 6 Anibal Sanchez
Remember to change the DB prefix (line 84) and the lastname customization (line 98).
258 6 Anibal Sanchez
259 5 Julien Recurt
h2. Bugs
260 5 Julien Recurt
261 5 Julien Recurt
 * In 1.0.1 if you got an error like "undefined method `stringify_keys!' for #<Array:...>) when logging" in see #6196
262 5 Julien Recurt
<pre>
263 5 Julien Recurt
<code class="ruby">
264 5 Julien Recurt
# Lines 97 from previous script
265 5 Julien Recurt
          retVal =
266 5 Julien Recurt
          {
267 5 Julien Recurt
            :firstname => resultRow[self.attr_firstname],
268 5 Julien Recurt
            :lastname => resultRow[self.attr_lastname],
269 5 Julien Recurt
            :mail => resultRow[self.attr_mail],
270 5 Julien Recurt
            :auth_source_id => self.id
271 5 Julien Recurt
          } if(onthefly_register?)
272 5 Julien Recurt
# ...
273 1 Andrew R Jackson
</code>
274 1 Andrew R Jackson
</pre>
275 6 Anibal Sanchez
276 6 Anibal Sanchez
 * In 2.0.3, the codefix for #6196 is still required, we've fixed the original code.