Java tutorial
/** * 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; } }