/*
* Copyright 2012 Janrain, Inc.
*
* 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 com.janrain.backplane2.server;
import com.janrain.backplane.common.DateTimeUtils;
import com.janrain.backplane.server.ExternalizableCore;
import com.janrain.backplane.server2.model.Backplane2MessageFields;
import com.janrain.commons.supersimpledb.SimpleDBException;
import com.janrain.commons.supersimpledb.message.AbstractMessage;
import com.janrain.commons.supersimpledb.message.MessageField;
import com.janrain.oauth2.TokenException;
import com.janrain.servlet.InvalidRequestException;
import com.janrain.util.RandomUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import org.jetbrains.annotations.NotNull;
import java.text.ParseException;
import java.util.*;
import static com.janrain.oauth2.OAuth2.*;
/**
* @author Tom Raney, Johnny Bufu
*/
public class Token extends ExternalizableCore {
/**
* Empty default constructor for AWS to use.
* Don't call directly.
*/
public Token() {}
/**
* Copy constructor (to help with migration).
*/
public Token(Map<String,String> data) throws SimpleDBException {
this(data.get(TokenField.ID.getFieldName()), data);
}
@Override
public String getIdValue() {
return get(TokenField.ID);
}
@Override
public Set<? extends MessageField> getFields() {
return EnumSet.allOf(TokenField.class);
}
@Override
public void validate() throws SimpleDBException {
super.validate();
if (getType().isPrivileged()) {
AbstractMessage.validateNotBlank(TokenField.ISSUED_TO_CLIENT_ID.getFieldName(), get(TokenField.ISSUED_TO_CLIENT_ID));
AbstractMessage.validateNotBlank(TokenField.CLIENT_SOURCE_URL.getFieldName(), get(TokenField.CLIENT_SOURCE_URL));
AbstractMessage.validateNotBlank(TokenField.BACKING_GRANTS.getFieldName(), get(Token.TokenField.BACKING_GRANTS));
} else {
Scope anonScope = getScope();
LinkedHashSet<String> buses = anonScope.getScopeMap().get(Backplane2MessageFields.BUS());
LinkedHashSet<String> channels = anonScope.getScopeMap().get(Backplane2MessageFields.CHANNEL());
if (buses == null || buses.size() > 1 || channels == null || channels.size() > 1) {
throw new SimpleDBException("invalid scope for anonymous token, must have exactly one bus and one channel specified: " + anonScope);
}
}
}
public GrantType getType() {
try {
return GrantType.valueOf(this.get(TokenField.TYPE));
} catch (IllegalArgumentException e) {
throw new IllegalStateException("Invalid GrantType on for TokenField.Type, should have been validated on token creation: " + this.get(TokenField.TYPE));
}
}
public Scope getScope() {
try {
return new Scope(this.get(TokenField.SCOPE));
} catch (TokenException e) {
throw new IllegalStateException("Invalid scope on get(), should have been validated on token creation: " + this.get(TokenField.SCOPE));
}
}
/**
* @return the token's expiration date, or null if the token never expires
*/
public Date getExpirationDate() {
String value = this.get(TokenField.EXPIRES);
try {
return StringUtils.isNotEmpty(value) ? DateTimeUtils.ISO8601.get().parse(value) : null;
} catch (ParseException e) {
throw new IllegalStateException("Invalid ISO8601 date for TokenField.EXPIRES, should have been validated on token creation: " + value);
} catch (NumberFormatException nfe) {
logger.error("Error parsing token date: " + value, nfe);
throw new IllegalStateException("Invalid ISO8601 date for TokenField.EXPIRES, should have been validated on token creation: " + value);
}
}
public String getScopeString() {
return get(TokenField.SCOPE);
}
public boolean isExpired() {
Date expires = getExpirationDate();
return expires != null && new Date().getTime() > expires.getTime();
}
public static boolean looksLikeOurToken(String tokenString) {
GrantType grantType = GrantType.fromTokenString(tokenString);
if (grantType == null) return false;
String tokenNoPrefix = tokenString.substring(grantType.getTokenPrefix().length());
return tokenNoPrefix.length() == TOKEN_LENGTH;
}
public Map<String, Object> response(final String refreshToken) {
final Date expires = getExpirationDate();
return new LinkedHashMap<String, Object>() {{
put(OAUTH2_TOKEN_TYPE_PARAM_NAME, OAUTH2_TOKEN_TYPE_BEARER);
put(OAUTH2_ACCESS_TOKEN_PARAM_NAME, getIdValue());
if (expires != null) put(OAUTH2_TOKEN_RESPONSE_EXPIRES, (expires.getTime() - new Date().getTime()) / 1000);
put(OAUTH2_SCOPE_PARAM_NAME, getScopeString());
if (refreshToken != null) put(OAUTH2_REFRESH_TOKEN_PARAM_NAME, refreshToken);
}};
}
public @NotNull List<String> getBackingGrants() {
String grants = get(TokenField.BACKING_GRANTS);
return StringUtils.isNotEmpty(grants) ? Arrays.asList(grants.split(GRANTS_SEPARATOR)) : new ArrayList<String>();
}
public static enum TokenField implements MessageField {
ID("id", true),
TYPE("type", true) {
@Override
public void validate(String value) throws SimpleDBException {
super.validate(value);
try {
GrantType.valueOf(value);
} catch (IllegalArgumentException e) {
throw new SimpleDBException("Invalid token type: " + value);
}
}
},
EXPIRES("expires", false) {
@Override
public void validate(String value) throws SimpleDBException {
super.validate(value);
try {
if (StringUtils.isNotEmpty(value)) {
DateTimeUtils.ISO8601.get().parse(value);
}
} catch (ParseException e) {
throw new SimpleDBException("Invalid token expiration date: " + value, e);
}
}
},
SCOPE("scope", true) {
@Override
public void validate(String value) throws SimpleDBException {
super.validate(value);
try {
new Scope(value);
} catch (TokenException e) {
throw new InvalidRequestException("Invalid scope: " + value);
}
}
},
ISSUED_TO_CLIENT_ID("issued_to_client", false),
CLIENT_SOURCE_URL("client_source_url", false) {
@Override
public void validate(String value) throws SimpleDBException {
super.validate(value);
if (StringUtils.isNotEmpty(value)) {
AbstractMessage.validateUrl(getFieldName(), value);
}
}
},
BACKING_GRANTS("backing_grants", false);
@Override
public String getFieldName() {
return fieldName;
}
@Override
public boolean isRequired() {
return required;
}
@Override
public void validate(String value) throws SimpleDBException {
if (isRequired()) validateNotBlank(getFieldName(), value);
}
// - PRIVATE
private String fieldName;
private boolean required = true;
private TokenField(String fieldName, boolean required) {
this.fieldName = fieldName;
this.required = required;
}
}
public static class Builder {
public Builder(GrantType type, String scope) {
this.type = type;
data.put(TokenField.TYPE.getFieldName(), type.toString());
data.put(TokenField.SCOPE.getFieldName(), scope);
}
public Builder expires(Date expires) {
data.put(TokenField.EXPIRES.getFieldName(), DateTimeUtils.ISO8601.get().format(expires));
return this;
}
public Builder issuedToClient(String clientId) {
data.put(TokenField.ISSUED_TO_CLIENT_ID.getFieldName(), clientId);
return this;
}
public Builder clientSourceUrl(String clientSourceUrl) {
data.put(TokenField.CLIENT_SOURCE_URL.getFieldName(), clientSourceUrl);
return this;
}
public Builder grants(List<String> grants) {
data.put(TokenField.BACKING_GRANTS.getFieldName(),
org.springframework.util.StringUtils.collectionToDelimitedString(grants, GRANTS_SEPARATOR));
return this;
}
public Token buildToken() throws SimpleDBException {
String id = type.getTokenPrefix() + RandomUtils.randomString(TOKEN_LENGTH);
data.put(TokenField.ID.getFieldName(), id);
return new Token(id, data);
}
// - PRIVATE
private final Map<String,String> data = new HashMap<String, String>();
private final GrantType type;
}
// - PRIVATE
private static final long serialVersionUID = -5883124096459804032L;
private static final Logger logger = Logger.getLogger(Token.class);
private static final int TOKEN_LENGTH = 20;
private static final String GRANTS_SEPARATOR = " ";
private Token(String id, Map<String,String> data) throws SimpleDBException {
super.init(id, data);
logger.debug("created token: " + this.toString());
}
}