AuthImpl.java
/***************************************************************************
Copyright 2008 Emily Estes
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
***************************************************************************/
package net.metanotion.formsauth;
import java.net.URLEncoder;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import net.metanotion.authident.AuthPassword;
import net.metanotion.authident.AuthUtils;
import net.metanotion.authident.CredentialedUserToken;
import net.metanotion.authident.LogoutEvent;
import net.metanotion.authident.UserToken;
import net.metanotion.util.SecureString;
import net.metanotion.util.StateMachine;
import net.metanotion.web.HttpStatus;
import net.metanotion.web.HttpValues;
import net.metanotion.web.RequestObject;
import net.metanotion.web.concrete.HttpUtil;
import net.metanotion.web.concrete.JsonUtil;
/** The concrete implementation of the AuthAPI/SimpleAuthAPI services, instances
of this class are created by {@link AuthFactory} and this is a package private class
not meant to be directly exposed to the user of the library. */
final class AuthImpl implements AuthAPI {
private static final Logger logger = LoggerFactory.getLogger(AuthImpl.class);
private static final HttpValues OK = HttpUtil.newStatus(HttpStatus.OK.codeNumber());
private static final HttpValues CREATED = HttpUtil.newStatus(HttpStatus.CREATED.codeNumber());
private static final HttpValues BAD_REQUEST = HttpUtil.newStatus(HttpStatus.BAD_REQUEST.codeNumber());
private static final HttpValues FORBIDDEN = HttpUtil.newStatus(HttpStatus.FORBIDDEN.codeNumber());
private static final HttpValues SEMANTIC_ERROR = HttpUtil.newStatus(HttpStatus.SEMANTIC_ERROR.codeNumber());
private final Object apiJson;
private final AuthPassword pw;
private final Validate v;
private final CreateAccount ca;
private final AuthMailer mailer;
private final AuthStore store;
public AuthImpl(final AuthStore store,
final AuthPassword pw,
final AuthMailer mailer,
final String prefix,
final Validate v,
final CreateAccount ca) {
this.store = store;
this.pw = pw;
this.v = v;
this.ca = ca;
this.mailer = mailer;
this.apiJson = JsonUtil.makeWebApi(AuthAPI.class, prefix);
}
// WebAuth methods
// 200 or 403
@Override public HttpValues login(final StateMachine sm,
final String username,
final SecureString password,
final String currentPage) {
logger.info(username);
return (AuthUtils.externalAuthenticate(pw, sm, username.trim(), password)) ? OK : FORBIDDEN;
}
// 200
@Override public HttpValues logout(final StateMachine sm) {
sm.nextState(LogoutEvent.LOGOUT);
return OK;
}
// WebAuthExtras methods
// 200 ALWAYS
@Override public HttpValues requestPasswordReset(final String username) {
if((username.trim().length() == 0)) {
return OK;
}
final String uname = username.trim();
try {
final ResetCode rc = store.createReset(uname);
logger.debug("Obtained the reset code.");
this.mailer.resetPassword(uname, Integer.toString(rc.ResetID), URLEncoder.encode(rc.UUID, "UTF-8"));
} catch (final Exception e) {
logger.debug("Could not process the password reset request.", e);
}
return OK;
}
// 200 or 400
@Override public HttpValues resetPassword(final long rid,
final String token,
final SecureString password,
final SecureString password2) {
try {
if((password.length() == 0) || (!(password.equals(password2)))) {
return BAD_REQUEST;
}
try {
final String username = store.lookupUsernameForResetCode(rid, token);
if (username != null) {
logger.debug("changing password");
this.pw.setPassword(username, password);
store.clearResetCode(username);
return OK;
}
} catch (final Exception e) {
logger.debug("Could not reset the password.", e);
}
} finally {
password.close();
password2.close();
}
return BAD_REQUEST;
}
// 201 on create, 422 otherwise
@Override public HttpValues createAccount(final StateMachine sm,
String username,
final SecureString password,
final SecureString password2,
final RequestObject ro) {
try {
/* We don't have to check if the parameters are null because the WebInterfaceDispatcher/ReflectionListDispatcher
ensure that only parameters annotated @Nullable can be null. */
username = username.trim();
if((username.length() == 0) || (password.length() == 0) || (!(password.equals(password2)))) {
return SEMANTIC_ERROR;
}
if(this.pw.getIdentity(username) != null) { return SEMANTIC_ERROR; }
this.v.validate(username, password, ro);
logger.debug("*******here");
final CredentialedUserToken ut = this.pw.createAuthentication(username, password);
logger.debug("** UT: " + ut);
final ValidationCode vc = store.createValidation(username);
try {
this.mailer.createAccount(username, Integer.toString(vc.ValidateID), URLEncoder.encode(vc.UUID, "UTF-8"));
} catch (final Exception e) {
/* If the mailer can't send the create account email, we'll log the event, but it really shouldn't
fail, and at this point a user account HAS been created, so we need to notify the client that the
account has been created, and if they need a another verification email sent, that's what the UI is
for. */
logger.debug("Could not send welcome/verfication email.", e);
}
try {
this.ca.account(username, ut, ro);
} catch (final Exception e) {
/* If the account creation event provided by the library user fails, we'll log the event, but it
really shouldn't fail, and at this point a user account HAS been created, so we need to notify the
client that the account has been created, and an audit process should pick this failure up on the
backend anyway. */
logger.debug("Account creation event failed unexepectedly", e);
}
sm.nextState(ut);
logger.debug("Done");
return CREATED;
} catch (final RuntimeException re) {
logger.debug("failed?", re);
} finally {
password.close();
password2.close();
}
return SEMANTIC_ERROR;
}
// 201 or 422
@Override public HttpValues addAccount(final UserToken ut,
String username,
final SecureString password,
final RequestObject ro) {
try {
if(username.trim().length() == 0) { return SEMANTIC_ERROR; }
username = username.trim();
this.v.validate(username, password, ro);
if(!AuthUtils.checkAuthenticatedIdentityMatches(pw, ut, AuthUtils.lookupUsername(pw, ut), password)) {
return FORBIDDEN;
}
this.pw.addAuthentication(ut, username);
try {
final ValidationCode vc = store.createValidation(username);
this.mailer.addAccount(username, Integer.toString(vc.ValidateID), URLEncoder.encode(vc.UUID, "UTF-8"));
} catch (final Exception e) {
logger.debug("Could not process the validation code and send the validation code to the added account.", e);
}
return CREATED;
} catch (final RuntimeException re) {
return SEMANTIC_ERROR;
} finally {
password.close();
}
}
// 200 or 403
@Override public HttpValues removeAccount(final UserToken ut, String username, final SecureString password) {
try {
username = username.trim();
if(!AuthUtils.checkAuthenticatedIdentityMatches(pw, ut, username, password)) {
return FORBIDDEN;
}
this.pw.deleteAuthentication(ut, username);
return OK;
} catch (final Exception e) {
logger.debug("Error deleting credential", e);
return FORBIDDEN;
} finally {
password.close();
}
}
// 200 or 403
@Override public HttpValues changePassword(final UserToken ut,
final SecureString oldPassword,
final SecureString newPassword,
final SecureString newPassword2) {
try {
if((newPassword.length() == 0) || (!(newPassword.equals(newPassword2)))) {
return FORBIDDEN;
}
final String username = AuthUtils.lookupUsername(pw, ut);
if(username == null) { return FORBIDDEN; }
if(!AuthUtils.checkAuthenticatedIdentityMatches(pw, ut, username, oldPassword)) {
return FORBIDDEN;
}
this.pw.setPassword(username, newPassword);
return OK;
} catch (final RuntimeException re) {
return FORBIDDEN;
} finally {
oldPassword.close();
newPassword.close();
newPassword2.close();
}
}
// 200 or 403
@Override public HttpValues sendAccountValidation(String username) {
username = username.trim();
if(username.length() == 0) { return OK; }
try {
final ValidationCode vc = store.createValidation(username);
this.mailer.validationEmail(username, Integer.toString(vc.ValidateID), URLEncoder.encode(vc.UUID, "UTF-8"));
} catch (final Exception e) {
logger.debug("Could not send account verfication email.", e);
}
return OK;
}
// 200 or 422
@Override public HttpValues validateAccount(final long vid, final String token) {
try {
final String username = store.lookupUsernameForValidationCode(vid, token);
if (username != null) {
store.validateUser(username);
return OK;
}
} catch (final Exception e) {
logger.debug("Could not properly validate the account.", e);
}
return SEMANTIC_ERROR;
}
private static String primaryUser(final UserToken ut, final List<Account> accts) {
return (ut instanceof CredentialedUserToken)
? ((CredentialedUserToken) ut).getCredential().toString()
: accts.get(0).username;
}
@Override public Object whoAmI(final UserToken ut) {
if(ut==null) { return SEMANTIC_ERROR; }
try {
final List<Account> accts = store.listAccounts(ut);
logger.debug(primaryUser(ut, accts));
return new AccountInfo(primaryUser(ut, accts), accts);
} catch (final Exception e) {
return SEMANTIC_ERROR;
}
}
@Override public Object api() { return this.apiJson; }
}