com.outerspacecat.openid.rp.Assertion.java Source code

Java tutorial

Introduction

Here is the source code for com.outerspacecat.openid.rp.Assertion.java

Source

/**
 * Copyright 2011 Caleb Richardson
 * 
 * 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.outerspacecat.openid.rp;

import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Multimap;
import com.outerspacecat.util.Base64;
import com.outerspacecat.util.Utils;
import java.io.Serializable;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.Charset;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Map;
import java.util.regex.Pattern;
import javax.annotation.concurrent.Immutable;
import javax.annotation.concurrent.ThreadSafe;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;

/**
 * A positive OpenID assertion.
 * 
 * @author Caleb Richardson
 */
@Immutable
@ThreadSafe
public final class Assertion implements Serializable {
    private final static long serialVersionUID = 1L;

    private final static Pattern NoncePattern = Pattern
            .compile("\\d{4}\\-\\d{2}\\-\\d{2}T\\d{2}\\:\\d{2}\\:\\d{2}Z[\\x21-\\x7E]{0,235}");

    private final String claimedId;

    private Assertion(final String claimedId) {
        Preconditions.checkNotNull(claimedId, "claimedId required");

        this.claimedId = claimedId;
    }

    /**
     * Returns whether or not {@code fields} <em>probably</em> represent a
     * positive assertion. Even if this method returns {@code true},
     * {@link #create(Association, Map, URI, NonceStore)} may still throw an
     * {@link java.io.IOException}.
     * 
     * @param fields the assertion fields. Must be non {@code null}.
     * @return whether or not the fields <em>probably</em> represent a positive
     *         assertion
     */
    public static boolean isPositive(final Map<String, String> fields) {
        return "http://specs.openid.net/auth/2.0".equals(fields.get("openid.ns"))
                && "id_res".equals(fields.get("openid.mode"));
    }

    /**
     * Creates a new assertion.
     * 
     * @param association the association to use to validate the assertion. Must
     *        be non {@code null}.
     * @param fields the assertion fields. Must be non {@code null}.
     * @param receivingUrl the url that is processing this assertion. Must be non
     *        {@code null}.
     * @param nonceStore a nonce store. Must be non {@code null};
     * @return a new assertion. Never {@code null}.
     * @throws InvalidAssertionException if an assertion cannot be created
     */
    public static Assertion create(final Association association, final Map<String, String> fields,
            final URI receivingUrl, final NonceStore nonceStore) throws InvalidAssertionException {
        Preconditions.checkNotNull(association, "association required");
        Preconditions.checkNotNull(fields, "fields required");
        Preconditions.checkNotNull(receivingUrl, "receivingUrl required");
        Preconditions.checkNotNull(nonceStore, "nonceStore required");

        String nsParam = fields.get("openid.ns");
        if (!"http://specs.openid.net/auth/2.0".equals(nsParam))
            throw new InvalidAssertionException("invalid ns: " + nsParam);

        String modeParam = fields.get("openid.mode");
        if (!"id_res".equals(modeParam))
            throw new InvalidAssertionException("invalid openid.mode: " + modeParam);

        String claimedIdParam = fields.get("openid.claimed_id");
        String identityParam = fields.get("openid.identity");
        if ((claimedIdParam == null) != (identityParam == null))
            throw new InvalidAssertionException(
                    "invalid openid.claimed_id and openid.identity: " + claimedIdParam + ", " + identityParam);

        String returnToParam = fields.get("openid.return_to");
        if (returnToParam == null)
            throw new InvalidAssertionException("missing openid.return_to");

        try {
            URI returnToUri = new URI(returnToParam);

            if (!(receivingUrl.getScheme() + "://" + receivingUrl.getRawAuthority() + receivingUrl.getRawPath())
                    .equals((returnToUri.getScheme() + "://" + returnToUri.getRawAuthority()
                            + returnToUri.getRawPath())))
                throw new InvalidAssertionException(null);

            Multimap<String, String> returnToParams = Utils.parseHttpQueryString(returnToUri.getRawQuery(),
                    Charset.forName("UTF-8"));

            for (Map.Entry<String, String> e : returnToParams.entries()) {
                if (!e.getValue().equals(fields.get(e.getKey())))
                    throw new InvalidAssertionException(
                            "missing return_to parameter, key=" + e.getKey() + ", return_to value=" + e.getValue()
                                    + ", recievingUrl value=" + fields.get(e.getKey()));
            }
        } catch (URISyntaxException e) {
            throw new InvalidAssertionException(
                    "invalid openid.return_to parameter, openid.return_to=" + returnToParam);
        }

        // openid.invalidate_handle is not checked because associations are never
        // reused

        String assocHandleParam = fields.get("openid.assoc_handle");
        if (!association.getHandle().equals(assocHandleParam))
            throw new InvalidAssertionException("invalid openid.assoc_handle, expected " + association.getHandle()
                    + ", got " + assocHandleParam);

        String responseSignedParam = fields.get("openid.signed");
        if (responseSignedParam == null)
            throw new InvalidAssertionException("missing openid.signed");

        String responseSigParam = fields.get("openid.sig");
        if (responseSigParam == null)
            throw new InvalidAssertionException("missing openid.sig");

        String responseNonceParam = fields.get("openid.response_nonce");
        if (responseNonceParam == null || !NoncePattern.matcher(responseNonceParam).matches())
            throw new InvalidAssertionException("invalid openid.response_nonce: " + responseNonceParam);

        // discovery information is not verified because none is ever gathered

        nonceStore.store(association.getEndpoint().getUri(), responseNonceParam);

        ImmutableSet<String> signedFields = ImmutableSet.copyOf(responseSignedParam.split(","));

        if (!signedFields.contains("op_endpoint"))
            throw new InvalidAssertionException("openid.signed must contain \"op_endpoint\"");
        if (!signedFields.contains("return_to"))
            throw new InvalidAssertionException("openid.signed must contain \"return_to\"");
        if (!signedFields.contains("response_nonce"))
            throw new InvalidAssertionException("openid.signed must contain \"response_nonce\"");
        if (!signedFields.contains("assoc_handle"))
            throw new InvalidAssertionException("openid.signed must contain \"assoc_handle\"");
        if (claimedIdParam != null && !signedFields.contains("claimed_id"))
            throw new InvalidAssertionException(
                    "openid.signed must contain \"claimed_id\" when claimed_id is in response");
        if (identityParam != null && !signedFields.contains("identity"))
            throw new InvalidAssertionException(
                    "openid.signed must contain \"identity\" when identity is in response");

        StringBuilder sb = new StringBuilder();
        for (String field : signedFields) {
            if (fields.containsKey("openid." + field)) {
                sb.append(field).append(":").append(fields.get("openid." + field)).append("\n");
            }
        }

        try {
            Mac mac = Mac.getInstance(association.getAssociationType().getAlgorithm());
            mac.init(new SecretKeySpec(association.getMacKey(), association.getAssociationType().getValue()));
            mac.update(sb.toString().getBytes(Charset.forName("UTF-8")));
            String calculatedSig = new String(Base64.encode(mac.doFinal()));

            if (!Utils.constantEquals(responseSigParam, calculatedSig))
                throw new InvalidAssertionException(
                        "invalid sig, expected " + calculatedSig + ", got " + responseSigParam);
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException(e);
        } catch (InvalidKeyException e) {
            throw new RuntimeException(e);
        }

        return new Assertion(claimedIdParam);
    }

    /**
     * Returns the claimed id specified by this assertion.
     * 
     * @return the claimed id specified by this assertion. Never {@code null}.
     */
    public String getClaimedId() {
        return claimedId;
    }
}