/*
 * Copyright 2014 JBoss 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 io.apiman.common.auth;

import io.apiman.common.logging.ApimanLoggerFactory;
import io.apiman.common.logging.IApimanLogger;

import java.util.Date;
import java.util.ServiceLoader;
import java.util.Set;
import java.util.TreeSet;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.codec.binary.StringUtils;
import org.apache.commons.codec.digest.DigestUtils;

/**
 * A simple class to generate and consume authentication tokens.  This is a very
 * naive token-based authentication mechanism.  It is useful for unit testing and
 * demos, but should not be used in production as it is not secure.  In 
 * production, it is better to use OAuth or some other mature bearer token style
 * authentication approach.
 *
 * @author eric.wittmann@redhat.com
 */
public class AuthTokenUtil {
    
    private static final IApimanLogger logger = ApimanLoggerFactory.getLogger(AuthTokenUtil.class);
    private static final ObjectMapper mapper = new ObjectMapper();
    private static ISharedSecretSource sharedSecretSource;
    static {
        ServiceLoader<ISharedSecretSource> loader = ServiceLoader.load(ISharedSecretSource.class);
        for (ISharedSecretSource source : loader) {
            sharedSecretSource = source;
            // Just use the first one you find.
            break;
        }
        if (sharedSecretSource == null) {
            logger.warn("Missing a Shared-Secret source for auth-token style authentication.  Defaulting to insecure, hard-coded value."); //$NON-NLS-1$
            sharedSecretSource = new ISharedSecretSource() {
                @Override
                public String getSharedSecret() {
                    // Hard coded shared secret.  Should not be used in production (see above warning).
                    return "2BB6E867BC7564162AB1FD26BE61E49365934FBA9B3E56B1323ABE104C798D5C"; //$NON-NLS-1$
                }
            };
        }
    }

    private AuthTokenUtil() {
    }

    /**
     * Produce a token suitable for transmission.  This will generate the auth token,
     * then serialize it to a JSON string, then Base64 encode the JSON.
     * @param principal the auth principal
     * @param roles the auth roles
     * @param expiresInMillis the number of millis to expiry
     * @return the token
     */
    public static final String produceToken(String principal, Set<String> roles, int expiresInMillis) {
        AuthToken authToken = createAuthToken(principal, roles, expiresInMillis);
        String json = toJSON(authToken);
        return StringUtils.newStringUtf8(Base64.encodeBase64(StringUtils.getBytesUtf8(json)));
    }

    /**
     * Consumes an auth token, validating it during the process.  If successful, the
     * {@link AuthToken} is returned.  If the token is invalid (expired or bad 
     * signature) then an {@link IllegalArgumentException} is thrown.  Any other 
     * error will result in a runtime exception.
     * @param encodedJson the raw token
     * @return the token
     * @throws IllegalArgumentException indicate that a method has been passed an 
     * illegal or inappropriate argument
     */
    public static final AuthToken consumeToken(String encodedJson) throws IllegalArgumentException {
        String json = StringUtils.newStringUtf8(Base64.decodeBase64(encodedJson));
        AuthToken token = fromJSON(json);
        validateToken(token);
        return token;
    }

    /**
     * Validates an auth token.  This checks the expiration time of the token against 
     * the current system time.  It also checks the validity of the signature.
     * @param token the authentication token
     * @throws IllegalArgumentException when the token is invalid
     */
    public static final void validateToken(AuthToken token) throws IllegalArgumentException {
        if (token.getExpiresOn().before(new Date())) {
            throw new IllegalArgumentException("Authentication token expired: " + token.getExpiresOn()); //$NON-NLS-1$
        }
        String validSig = generateSignature(token);
        if (token.getSignature() == null || !token.getSignature().equals(validSig)) {
            throw new IllegalArgumentException("Missing or invalid signature on the auth token."); //$NON-NLS-1$
        }
    }

    /**
     * Creates an auth token.
     * @param principal the auth principal
     * @param roles the auth roles
     * @param expiresInMillis the number of millis to expiry
     * @return the auth token
     */
    public static final AuthToken createAuthToken(String principal, Set<String> roles, int expiresInMillis) {
        AuthToken token = new AuthToken();
        token.setIssuedOn(new Date());
        token.setExpiresOn(new Date(System.currentTimeMillis() + expiresInMillis));
        token.setPrincipal(principal);
        token.setRoles(roles);
        signAuthToken(token);
        return token;
    }

    /**
     * Adds a digital signature to the auth token.
     * @param token the auth token
     */
    public static final void signAuthToken(AuthToken token) {
        String signature = generateSignature(token);
        token.setSignature(signature);
    }

    /**
     * Generates a signature for the given token.
     * @param token
     */
    private static String generateSignature(AuthToken token) {
        StringBuilder builder = new StringBuilder();
        builder.append(token.getPrincipal());
        builder.append("||"); //$NON-NLS-1$
        builder.append(token.getExpiresOn().getTime());
        builder.append("||"); //$NON-NLS-1$
        builder.append(token.getIssuedOn().getTime());
        builder.append("||"); //$NON-NLS-1$
        TreeSet<String> roles = new TreeSet<>(token.getRoles());
        boolean first = true;
        for (String role : roles) {
            if (first) {
                first = false;
            } else {
                builder.append(","); //$NON-NLS-1$
            }
            builder.append(role);
        }
        builder.append("||"); //$NON-NLS-1$
        builder.append(sharedSecretSource.getSharedSecret());
        return DigestUtils.sha256Hex(builder.toString());
    }

    /**
     * Convert the auth token to a JSON string.
     * @param token the auth token
     * @return the token as json
     */
    public static final String toJSON(AuthToken token) {
        try {
            return mapper.writer().writeValueAsString(token);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
    
    /**
     * Read the auth token from the JSON string.
     * @param json the token as a json string
     * @return the token
     */
    public static final AuthToken fromJSON(String json) {
        try {
            return mapper.reader(AuthToken.class).readValue(json);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

}
