Skip to content

Commit

Permalink
CookieAuthProvider for Remember Me functionality.
Browse files Browse the repository at this point in the history
  • Loading branch information
smola committed Mar 20, 2013
1 parent 8f45989 commit da6a38b
Show file tree
Hide file tree
Showing 22 changed files with 486 additions and 17 deletions.
12 changes: 11 additions & 1 deletion code/app/com/feth/play/module/pa/PlayAuthenticate.java
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,15 @@ public abstract static class Resolver {
*/
public abstract Call afterLogout();

/**
* Do something after a successful authentication.
*
* This is called when a login was successful. Either after
* an explicit login or after sign up. Make sure you call this
* if you manually login the user (i.e. PlayAuthenticate.storeUser()).
*/
public void onAuthSuccess(final Context ctx, final AuthUser authUser) { }

public Call onException(final AuthException e) {
return null;
}
Expand Down Expand Up @@ -394,6 +403,7 @@ public static Result link(final Context context, final boolean link) {
public static Result loginAndRedirect(final Context context,
final AuthUser loginUser) {
storeUser(context.session(), loginUser);
getResolver().onAuthSuccess(context, loginUser);
return Controller.redirect(getJumpUrl(context));
}

Expand Down Expand Up @@ -431,7 +441,7 @@ private static AuthUser signupUser(final AuthUser u) throws AuthException {
public static Result handleAuthentication(final String provider,
final Context context, final Object payload) {
final AuthProvider ap = getProvider(provider);
if (ap == null) {
if (ap == null || !ap.isManaged()) {
// Provider wasn't found and/or user was fooling with our stuff -
// tell him off:
return Controller.notFound(Messages.get(
Expand Down
4 changes: 4 additions & 0 deletions code/app/com/feth/play/module/pa/providers/AuthProvider.java
Original file line number Diff line number Diff line change
Expand Up @@ -121,5 +121,9 @@ public AuthUser getSessionAuthUser(final String id, final long expires) {
}

public abstract boolean isExternal();

public boolean isManaged() {
return true;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
package com.feth.play.module.pa.providers.cookie;

import java.util.Arrays;
import java.util.List;
import java.util.UUID;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import play.Application;
import play.Play;
import play.mvc.Http.Context;
import play.mvc.Http.Cookie;
import play.mvc.Http.Cookies;

import com.feth.play.module.pa.PlayAuthenticate;
import com.feth.play.module.pa.providers.AuthProvider;
import com.feth.play.module.pa.user.AuthUser;

public abstract class CookieAuthProvider extends AuthProvider {

private static final Logger log = LoggerFactory.getLogger(CookieAuthProvider.class);

protected static final String PROVIDER_KEY = "cookie";

protected static final String SESSION_KEY_DO_NOT_REMEMBER = "pa.cookie.do_not_remember";

private static final String SETTING_KEY_COOKIE_NAME = "cookieName";
private static final String SETTING_KEY_TIMEOUT = "timeout";
private static final String SETTING_KEY_SECURE_ONLY = "secureOnly";
private static final String SETTING_KEY_PATH = "path";

@Override
protected List<String> neededSettingKeys() {
return Arrays.asList(
SETTING_KEY_COOKIE_NAME,
SETTING_KEY_TIMEOUT,
SETTING_KEY_SECURE_ONLY,
SETTING_KEY_PATH
);
}

public CookieAuthProvider(final Application app) {
super(app);
}

@Override
public String getKey() {
return PROVIDER_KEY;
}

protected static enum CheckResult {
SUCCESS,
INVALID_TOKEN,
MISSING_SERIES,
EXPIRED,
ERROR
}

protected abstract CheckResult check(final CookieAuthUser cookieAuthUser);

protected void save(final CookieAuthUser cookieAuthUser, final AuthUser loginUser) {
PlayAuthenticate.getUserService().link(loginUser, cookieAuthUser);
}

protected abstract void renew(final CookieAuthUser cookieAuthUser, final String newToken);

/**
* Deletes an auth cookie. This assumes that any persisted auth cookie
* with the given series (cookieAuthUser.getSeries()) is deleted.
*
* @param cookieAuthUser
*/
protected void deleteSeries(final CookieAuthUser cookieAuthUser) {
PlayAuthenticate.getUserService().unlink(cookieAuthUser);
}

protected void potentialTheft(final CookieAuthUser cookieAuthUser) {
log.warn("Potential cookie theft: {}", cookieAuthUser.getId());
}

protected String getRandom() {
return UUID.randomUUID().toString();
}

protected String getNewSeries() {
return getRandom();
}

protected String getNewToken() {
return getRandom();
}

protected int getTimeout() {
return getConfiguration().getInt(SETTING_KEY_TIMEOUT);
}

public String getCookieName() {
return getConfiguration().getString(SETTING_KEY_COOKIE_NAME);
}

protected boolean isSecureOnly() {
return !Play.isDev() && getConfiguration().getBoolean(SETTING_KEY_SECURE_ONLY);
}

protected String getPath() {
return getConfiguration().getString(SETTING_KEY_PATH);
}

public void remember(final Context ctx) {
remember(ctx, PlayAuthenticate.getUser(ctx));
}

public void remember(final Context ctx, final AuthUser authUser) {
if (authUser == null) {
return;
}

if ("true".equals(ctx.session().get(SESSION_KEY_DO_NOT_REMEMBER))) {
ctx.session().remove(SESSION_KEY_DO_NOT_REMEMBER);
ctx.response().discardCookie(getCookieName());
return;
}

CookieAuthUser cookieAuthUser = null;
if (authUser instanceof CookieAuthUser) {
final CookieAuthUser oldCookieAuthUser = (CookieAuthUser) authUser;
cookieAuthUser = oldCookieAuthUser.renew(getNewToken());
renew(oldCookieAuthUser, cookieAuthUser.getToken());
} else {
cookieAuthUser = new CookieAuthUser(getNewSeries(), getNewToken());
save(cookieAuthUser, authUser);
}

ctx.response().setCookie(
getCookieName(),
cookieAuthUser.toCookieValue(),
getTimeout(), // MaxAge in seconds
getPath(), // Path
null,
isSecureOnly(),
true); //httpOnly, not javascript!
}

public void forget(final Context ctx) {
final AuthUser authUser = PlayAuthenticate.getUser(ctx);
ctx.response().discardCookie(getCookieName());
if (authUser instanceof CookieAuthUser) {
deleteSeries((CookieAuthUser)authUser);
}
}

public CookieAuthUser authenticate(final Context ctx) {
return (CookieAuthUser) authenticate(ctx, null);
}

@Override
public Object authenticate(final Context ctx, final Object payload) {
final Cookies cookies = ctx.request().cookies();
final Cookie cookie = cookies.get(getCookieName());
if (cookie == null) {
log.trace("authenticate() - cookie ({}) is null", getCookieName());
return null;
}

final CookieAuthUser cookieAuthUser = CookieAuthUser.fromCookieValue(cookie.value());
if (cookieAuthUser == null) {
log.warn("Discarding marlformed cookie: {}", cookie.value());
ctx.response().discardCookie(getCookieName());
return null;
}

final CheckResult checkResult = check(cookieAuthUser);

if (null == checkResult ||
CheckResult.MISSING_SERIES == checkResult ||
CheckResult.EXPIRED == checkResult ||
CheckResult.ERROR == checkResult) {

log.debug("Discarding expired or missing ({}) cookie: {}",
(checkResult == null)? null : checkResult.name(), cookie.value());
ctx.response().discardCookie(getCookieName());
return null;

} else if (CheckResult.SUCCESS == checkResult) {

log.trace("Renew cookie: {}", cookie.value());
remember(ctx, cookieAuthUser);
PlayAuthenticate.storeUser(ctx.session(), cookieAuthUser);
return cookieAuthUser;

} else if (CheckResult.INVALID_TOKEN == checkResult) {
potentialTheft(cookieAuthUser);
deleteSeries(cookieAuthUser);
ctx.response().discardCookie(getCookieName());
return null;

}
return null;
}

public void doNotRemember(Context ctx) {
ctx.session().put(SESSION_KEY_DO_NOT_REMEMBER, "true");
}

@Override
public boolean isExternal() {
return false;
}

@Override
public boolean isManaged() {
return false;
}

public static CookieAuthProvider getCookieProvider() {
return (CookieAuthProvider) PlayAuthenticate.getProvider(PROVIDER_KEY);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package com.feth.play.module.pa.providers.cookie;

import com.feth.play.module.pa.user.AuthUser;
import com.feth.play.module.pa.user.TokenIdentity;

public class CookieAuthUser extends AuthUser implements TokenIdentity {

/**
*
*/
private static final long serialVersionUID = 1L;

private final String series;
private final String token;

public CookieAuthUser(final String series, final String token) {
this.series = series;
this.token = token;
}

@Override
public String getId() {
return series;
}

@Override
public String getProvider() {
return CookieAuthProvider.PROVIDER_KEY;
}

public String getSeries() {
return series;
}

@Override
public String getToken() {
return token;
}

public CookieAuthUser renew(final String newToken) {
return new CookieAuthUser(series, newToken);
}

public String toCookieValue() {
return String.format("%s|%s", series, token);
}

public static CookieAuthUser fromCookieValue(final String value) {
final String[] fields = value.split("\\|");
if (fields.length != 2) {
return null;
}
return new CookieAuthUser(fields[0], fields[1]);
}

}
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
package com.feth.play.module.pa.providers.oauth1;

import com.feth.play.module.pa.user.AuthUser;
import com.feth.play.module.pa.user.TokenIdentity;

public abstract class OAuth1AuthUser extends AuthUser {
public abstract class OAuth1AuthUser extends AuthUser implements TokenIdentity {

/**
*
Expand Down Expand Up @@ -34,4 +35,10 @@ public String getId() {
public String getState() {
return state;
}

@Override
public String getToken() {
return getOAuth1AuthInfo().getAccessToken();
}

}
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
package com.feth.play.module.pa.providers.oauth2;



import com.feth.play.module.pa.user.AuthUser;
import com.feth.play.module.pa.user.TokenIdentity;

public abstract class OAuth2AuthUser extends AuthUser {
public abstract class OAuth2AuthUser extends AuthUser implements TokenIdentity {

/**
*
Expand Down Expand Up @@ -40,4 +39,9 @@ public long expires() {
public String getState() {
return state;
}

public String getToken() {
return getOAuth2AuthInfo().getAccessToken();
}

}
6 changes: 6 additions & 0 deletions code/app/com/feth/play/module/pa/service/UserService.java
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,12 @@ public interface UserService {
*/
public AuthUser link(final AuthUser oldUser, final AuthUser newUser);

/**
* Unlinks an account from a user. It is up to the implementor if the user
* should be removed if there are no more accounts linked to it.
*/
public void unlink(final AuthUser knownUser);

/**
* Gets called when a user logs in - you might make profile updates here with data coming from the login provider
* or bump a last-logged-in date
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,10 @@ public AuthUser update(AuthUser knownUser) {
// Default: just do nothing when user logs in again
return knownUser;
}

@Override
public void unlink(final AuthUser knownUser) {
// Default: Refuse to unlink, throw exception,
throw new UnsupportedOperationException("unlink is not supported");
}
}
7 changes: 7 additions & 0 deletions code/app/com/feth/play/module/pa/user/TokenIdentity.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.feth.play.module.pa.user;

public interface TokenIdentity {

public String getToken();

}
Loading

0 comments on commit da6a38b

Please sign in to comment.