001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.wicket.authentication.strategy;
018
019import java.util.UUID;
020
021import org.apache.wicket.authentication.IAuthenticationStrategy;
022import org.apache.wicket.util.cookies.CookieDefaults;
023import org.apache.wicket.util.cookies.CookieUtils;
024import org.apache.wicket.util.crypt.ICrypt;
025import org.apache.wicket.util.crypt.SunJceCrypt;
026import org.apache.wicket.util.lang.Args;
027import org.apache.wicket.util.string.Strings;
028import org.slf4j.Logger;
029import org.slf4j.LoggerFactory;
030
031/**
032 * Wicket's default implementation of an authentication strategy. It'll concatenate username and
033 * password, encrypt it and put it into one Cookie.
034 * <p>
035 * Note: To support automatic authentication across application restarts you have to use
036 * the constructor {@link DefaultAuthenticationStrategy#DefaultAuthenticationStrategy(String, ICrypt)}.
037 * 
038 * @author Juergen Donnerstag
039 */
040public class DefaultAuthenticationStrategy implements IAuthenticationStrategy
041{
042        private static final Logger logger = LoggerFactory.getLogger(DefaultAuthenticationStrategy.class);
043
044        /** The cookie name to store the username and password */
045        protected final String cookieKey;
046
047        /**
048         * @deprecated no longer used TODO remove in Wicket 10
049         */
050        @Deprecated(forRemoval = true)
051        protected final String encryptionKey = null;
052
053        /** The separator used to concatenate the username and password */
054        protected final String VALUE_SEPARATOR = "-sep-";
055
056        /** Cookie utils with default settings */
057        private CookieUtils cookieUtils;
058
059        /** Use to encrypt cookie values for username and password. */
060        private ICrypt crypt;
061
062        /**
063         * Constructor
064         * 
065         * @param cookieKey
066         *            The name of the cookie
067         *            
068         * @deprecated supply a crypt instead TODO remove in Wicket 10
069         */
070        @Deprecated(forRemoval = true)
071        public DefaultAuthenticationStrategy(final String cookieKey)
072        {
073                this(cookieKey, defaultEncryptionKey());
074        }
075
076        private static String defaultEncryptionKey()
077        {
078                return UUID.randomUUID().toString();
079        }
080
081        /**
082         * @deprecated supply a crypt instead TODO remove in Wicket 10
083         */
084        @Deprecated(forRemoval = true)
085        public DefaultAuthenticationStrategy(final String cookieKey, final String encryptionKey)
086        {
087                this(cookieKey, defaultCrypt(encryptionKey));
088        }
089
090        private static ICrypt defaultCrypt(String encryptionKey)
091        {
092                byte[] salt = SunJceCrypt.randomSalt();
093
094                SunJceCrypt crypt = new SunJceCrypt(salt, 1000);
095                crypt.setKey(encryptionKey);
096                return crypt;
097        }
098
099        /**
100         * This is the recommended constructor to be used, which allows automatic authentication across
101         * application restarts.  
102         * 
103         * @param cookieKey
104         *            The name of the cookie
105         * @param crypt
106         *            the crypt
107         */
108        public DefaultAuthenticationStrategy(final String cookieKey, ICrypt crypt)
109        {
110                this.cookieKey = Args.notEmpty(cookieKey, "cookieKey");
111                this.crypt = Args.notNull(crypt, "crypt");
112        }
113
114        /**
115         * Make sure you always return a valid CookieUtils
116         * 
117         * @return CookieUtils
118         */
119        protected CookieUtils getCookieUtils()
120        {
121                if (cookieUtils == null)
122                {
123                        CookieDefaults settings = new CookieDefaults();
124                        settings.setHttpOnly(true);
125                        cookieUtils = new CookieUtils(settings);
126                }
127                return cookieUtils;
128        }
129
130        /**
131         * @return The crypt engine to be used
132         */
133        protected ICrypt getCrypt()
134        {
135                return crypt;
136        }
137
138        @Override
139        public String[] load()
140        {
141                String value = getCookieUtils().load(cookieKey);
142                if (Strings.isEmpty(value) == false)
143                {
144                        try
145                        {
146                                value = getCrypt().decryptUrlSafe(value);
147                        }
148                        catch (RuntimeException e)
149                        {
150                                logger.info(
151                                        "Error decrypting login cookie: {}. The cookie will be deleted. Possible cause is that a session-relative encryption key was used to encrypt this cookie while this decryption attempt is happening in a different session, eg user coming back to the application after session expiration",
152                                        cookieKey);
153                                getCookieUtils().remove(cookieKey);
154                                value = null;
155                        }
156                        return decode(value);
157                }
158
159                return null;
160        }
161
162        /**
163         * This method will decode decrypted cookie value based on application needs
164         *
165         * @param value decrypted cookie value
166         * @return decomposed values array, or null in case cookie value was empty.
167         */
168        protected String[] decode(String value) {
169                if (Strings.isEmpty(value) == false)
170                {
171                        String username = null;
172                        String password = null;
173
174                        String[] values = value.split(VALUE_SEPARATOR);
175                        if ((values.length > 0) && (Strings.isEmpty(values[0]) == false))
176                        {
177                                username = values[0];
178                        }
179                        if ((values.length > 1) && (Strings.isEmpty(values[1]) == false))
180                        {
181                                password = values[1];
182                        }
183
184                        return new String[] { username, password };
185                }
186                return null;
187        }
188
189        @Override
190        public void save(final String credential, final String... extraCredentials)
191        {
192                String encryptedValue = getCrypt().encryptUrlSafe(encode(credential, extraCredentials));
193
194                getCookieUtils().save(cookieKey, encryptedValue);
195        }
196
197        /**
198         * This method can be overridden to provide different encoding mechanism
199         *
200         * @param credential
201         * @param extraCredentials
202         * @return String representation of the parameters given
203         */
204        protected String encode(final String credential, final String... extraCredentials)
205        {
206                StringBuilder value = new StringBuilder(credential);
207                if (extraCredentials != null)
208                {
209                        for (String extraCredential : extraCredentials)
210                        {
211                                value.append(VALUE_SEPARATOR).append(extraCredential);
212                        }
213                }
214                return value.toString();
215        }
216
217        @Override
218        public void remove()
219        {
220                getCookieUtils().remove(cookieKey);
221        }
222}