Java tutorial
/* * Copyright (c) 2014 3 Round Stones Inc., Some Rights Reserved * * 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 org.callimachusproject.behaviours; import static org.callimachusproject.util.PercentCodec.decode; import static org.callimachusproject.util.PercentCodec.encode; import java.io.IOException; import java.math.BigInteger; import java.nio.charset.Charset; import java.security.GeneralSecurityException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.text.DateFormat; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Collection; import java.util.Date; import java.util.GregorianCalendar; import java.util.LinkedHashMap; import java.util.Map; import java.util.TimeZone; import javax.crypto.Mac; import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; import javax.xml.datatype.DatatypeConfigurationException; import javax.xml.datatype.DatatypeFactory; import javax.xml.datatype.XMLGregorianCalendar; import org.apache.commons.codec.binary.Base64; import org.apache.http.HttpResponse; import org.apache.http.HttpVersion; import org.apache.http.entity.StringEntity; import org.apache.http.message.BasicHttpResponse; import org.callimachusproject.auth.AuthorizationManager; import org.callimachusproject.auth.DetachedAuthenticationManager; import org.callimachusproject.concepts.AuthenticationManager; import org.callimachusproject.repository.CalliRepository; import org.callimachusproject.traits.CalliObject; import org.openrdf.OpenRDFException; import org.openrdf.model.Literal; import org.openrdf.model.Resource; import org.openrdf.model.Statement; import org.openrdf.model.URI; import org.openrdf.model.Value; import org.openrdf.model.ValueFactory; import org.openrdf.model.vocabulary.DCTERMS; import org.openrdf.model.vocabulary.RDF; import org.openrdf.model.vocabulary.RDFS; import org.openrdf.query.BindingSet; import org.openrdf.query.QueryLanguage; import org.openrdf.query.TupleQuery; import org.openrdf.query.TupleQueryResult; import org.openrdf.repository.RepositoryException; import org.openrdf.repository.RepositoryResult; import org.openrdf.repository.object.ObjectConnection; public abstract class AuthenticationManagerSupport implements CalliObject, AuthenticationManager { private static final int MAX_NONCE_AGE = 300000; // nonce timeout of 5min private static final String CALLI = "http://callimachusproject.org/rdf/2009/framework#"; private static final String DERIVED_FROM = "http://www.w3.org/ns/prov#wasDerivedFrom"; private static final String FOAF_DEPICTION = "http://xmlns.com/foaf/0.1/depiction"; private static final String HAS_COMPONENT = CALLI + "hasComponent"; private static final String PARTY = CALLI + "Party"; private static final String USER = CALLI + "User"; private static final String EMAIL = CALLI + "email"; private static final String PROV = "http://www.w3.org/ns/prov#"; private static final String PREFIX = "PREFIX rdfs:<http://www.w3.org/2000/01/rdf-schema#>\n" + "PREFIX calli:<http://callimachusproject.org/rdf/2009/framework#>\n"; private static final String IDENTITY_NAME_EMAIL = PREFIX + "SELECT ?name ?email { $identity rdfs:label ?name; calli:email ?email }"; public HttpResponse openIDProvider(String parameters) throws ParseException, OpenRDFException, IOException, GeneralSecurityException { Map<String, String> map = keyByName(parameters); String mode = map.get("openid.mode"); if ("associate".equals(mode)) { String body = "ns=http://specs.openid.net/auth/2.0&error=use+direct+verification&error_code=unsupported-type"; BasicHttpResponse resp = new BasicHttpResponse(HttpVersion.HTTP_1_1, 400, "Unsupported"); resp.setHeader("Content-Type", "text/plain"); resp.setEntity(new StringEntity(body)); return resp; } else if ("checkid_setup".equals(mode)) { String self = this.getResource().stringValue(); String return_to = self + "?checkid&" + parameters; if (!map.containsKey("openid.return_to")) { return_to = "/"; } String location = self + "?login&return_to=" + encode(return_to); BasicHttpResponse resp = new BasicHttpResponse(HttpVersion.HTTP_1_1, 303, "See Other"); resp.setHeader("Location", location); resp.setHeader("Content-Type", "text/plain"); resp.setEntity(new StringEntity(location)); return resp; } else if ("checkid_immediate".equals(mode)) { String self = this.getResource().stringValue(); String location = self + "?checkid&" + parameters; BasicHttpResponse resp = new BasicHttpResponse(HttpVersion.HTTP_1_1, 303, "See Other"); resp.setHeader("Location", location); resp.setHeader("Content-Type", "text/plain"); resp.setEntity(new StringEntity(location)); return resp; } else if ("check_authentication".equals(mode)) { String user_fullname = map.get("openid.ax.value.fullname"); String user_email = map.get("openid.ax.value.email"); String user_uri = map.get("openid.identity"); String response_nonce = map.get("openid.response_nonce"); String time = response_nonce.substring(0, response_nonce.indexOf('Z') + 1); String self = this.getResource().stringValue(); StringBuilder hash = new StringBuilder(user_uri); if (user_fullname != null && user_email != null) { hash.append(':').append(user_fullname).append(':').append(user_email); } String nonce = time + hash(hash.toString(), "MD5"); StringBuilder sig = new StringBuilder(); sig.append("openid.op_endpoint=").append(encode(self)); sig.append("&openid.identity=").append(encode(user_uri)); sig.append("&openid.claimed_id=").append(encode(user_uri)); sig.append("&openid.return_to=").append(encode(map.get("openid.return_to"))); sig.append("&openid.response_nonce=").append(encode(nonce)); Date now = new Date(); DateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); df.setTimeZone(TimeZone.getTimeZone("UTC")); Date then = df.parse(time); if (!nonce.equals(response_nonce)) { String body = "ns:http://specs.openid.net/auth/2.0\nis_valid:false"; BasicHttpResponse resp = new BasicHttpResponse(HttpVersion.HTTP_1_1, 400, "Bad openid.response_nonce"); resp.setHeader("Content-Type", "text/plain"); resp.setEntity(new StringEntity(body)); return resp; } else if (!sig(sig.toString()).equals(map.get("openid.sig"))) { String body = "ns:http://specs.openid.net/auth/2.0\nis_valid:false"; BasicHttpResponse resp = new BasicHttpResponse(HttpVersion.HTTP_1_1, 400, "Bad openid.sig"); resp.setHeader("Content-Type", "text/plain"); resp.setEntity(new StringEntity(body)); return resp; } else if (then.getTime() + MAX_NONCE_AGE < now.getTime()) { String body = "ns:http://specs.openid.net/auth/2.0\nis_valid:false"; BasicHttpResponse resp = new BasicHttpResponse(HttpVersion.HTTP_1_1, 400, "Request Has Expired"); resp.setHeader("Content-Type", "text/plain"); resp.setEntity(new StringEntity(body)); return resp; } else { String body = "ns:http://specs.openid.net/auth/2.0\nis_valid:true"; BasicHttpResponse resp = new BasicHttpResponse(HttpVersion.HTTP_1_1, 200, "OK"); resp.setHeader("Content-Type", "text/plain"); resp.setHeader("Cache-Control", "no-store"); resp.setEntity(new StringEntity(body)); return resp; } } else { String location = this.getResource().stringValue() + "?view"; BasicHttpResponse resp = new BasicHttpResponse(HttpVersion.HTTP_1_1, 303, "See Other"); resp.setHeader("Location", location); resp.setHeader("Content-Type", "text/plain"); resp.setEntity(new StringEntity(location)); return resp; } } public HttpResponse openIDProviderReturn(String parameters, String identity) throws OpenRDFException, IOException, GeneralSecurityException { Map<String, String> map = keyByName(parameters); String return_to = map.get("openid.return_to"); StringBuilder sb = new StringBuilder(); if (return_to != null && return_to.length() > 0) { String self = this.getResource().stringValue(); StringBuilder hash = new StringBuilder(identity); String[] nameEmail = getNameEmail(identity); DateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); df.setTimeZone(TimeZone.getTimeZone("UTC")); String now = df.format(new Date()); sb.append(return_to); if (return_to.indexOf('?') >= 0) { sb.append('&'); } else { sb.append('?'); } sb.append("openid.ns=http://specs.openid.net/auth/2.0"); sb.append("&openid.mode=id_res"); if (nameEmail != null) { sb.append("&openid.ns.ax=http://openid.net/srv/ax/1.0"); sb.append("&openid.ax.mode=fetch_response"); sb.append("&openid.ax.type.email=http://axschema.org/contact/email"); sb.append("&openid.ax.type.fullname=http://axschema.org/namePerson"); sb.append("&openid.ax.value.email=").append(encode(nameEmail[1])); sb.append("&openid.ax.value.fullname=").append(encode(nameEmail[0])); hash.append(':').append(nameEmail[0]).append(':').append(nameEmail[1]); } String response_nonce = now + hash(hash.toString(), "MD5"); StringBuilder sig = new StringBuilder(); sig.append("openid.op_endpoint=").append(encode(self)); sig.append("&openid.identity=").append(encode(identity)); sig.append("&openid.claimed_id=").append(encode(identity)); sig.append("&openid.return_to=").append(encode(return_to)); sig.append("&openid.response_nonce=").append(encode(response_nonce)); sb.append("&").append(sig); sb.append("&openid.signed=").append("op_endpoint,identity,claimed_id,return_to,response_nonce"); sb.append("&openid.sig=").append(encode(sig(sig.toString()))); } else { sb.append("/"); } String location = sb.toString(); BasicHttpResponse resp = new BasicHttpResponse(HttpVersion.HTTP_1_1, 303, "See Other"); resp.setHeader("Location", location); resp.setHeader("Cache-Control", "no-store"); resp.setHeader("Content-Type", "text/plain"); resp.setEntity(new StringEntity(location)); return resp; } public void resetCache() throws RepositoryException { this.getObjectConnection().commit(); getCalliRepository().resetCache(); } public boolean isProtected(String url) throws OpenRDFException, IOException { return getManager().isProtected(url); } /** * Called from digest.ttl */ public void registerUser(Resource invitedUser, String digestUser, String email, String fullname) throws OpenRDFException, IOException { ObjectConnection con = this.getObjectConnection(); ValueFactory vf = con.getValueFactory(); registerUser(invitedUser, vf.createURI(digestUser), email, fullname, con); getCalliRepository().resetCache(); } /** * Called from realm.ttl and digest-user.ttl */ public String getUserIdentifier(String method, Collection<String> tokens) throws OpenRDFException, IOException { DetachedAuthenticationManager mgr = getManager(); if (mgr == null) return null; return mgr.getUserIdentifier(method, tokens, this.getObjectConnection()); } /** * Called from realm.ttl */ public String getUserLogin(Collection<String> tokens) throws OpenRDFException, IOException { DetachedAuthenticationManager mgr = getManager(); if (mgr == null) return null; return mgr.getUserLogin(tokens, this.getObjectConnection()); } /** * called from realm.ttl */ public String[] getUsernameSetCookie(Collection<String> tokens) throws OpenRDFException, IOException { DetachedAuthenticationManager mgr = getManager(); if (mgr == null) return null; return mgr.getUsernameSetCookie(tokens, this.getObjectConnection()); } protected DetachedAuthenticationManager getManager() throws OpenRDFException, IOException { CalliRepository repo = getCalliRepository(); AuthorizationManager manager = repo.getAuthorizationManager(); return manager.getAuthenticationManager(this.getResource()); } private String sig(String text) throws OpenRDFException, IOException, GeneralSecurityException { String secret = this.getRealm().getOriginSecret(); SecretKey key = new SecretKeySpec(readBytes(secret), "HmacSHA256"); Mac m = Mac.getInstance("HmacSHA256"); m.init(key); m.update(text.getBytes("UTF-8")); return Base64.encodeBase64String(m.doFinal()); } private byte[] readBytes(String string) { if (Base64.isBase64(string)) return Base64.decodeBase64(string); return string.getBytes(Charset.forName("UTF-8")); } private String[] getNameEmail(String identity) throws OpenRDFException { ObjectConnection con = this.getObjectConnection(); TupleQuery qry = con.prepareTupleQuery(QueryLanguage.SPARQL, IDENTITY_NAME_EMAIL); qry.setBinding("identity", con.getValueFactory().createURI(identity)); TupleQueryResult results = qry.evaluate(); try { if (results.hasNext()) { BindingSet result = results.next(); return new String[] { result.getValue("name").stringValue(), result.getValue("email").stringValue() }; } else { return null; } } finally { results.close(); } } private void registerUser(Resource invitedUser, URI regUser, String email, String fullname, ObjectConnection con) throws OpenRDFException, IOException { ValueFactory vf = con.getValueFactory(); RepositoryResult<Statement> stmts; stmts = con.getStatements((Resource) null, null, invitedUser); try { while (stmts.hasNext()) { moveTo(regUser, stmts.next(), con); } } finally { stmts.close(); } stmts = con.getStatements(invitedUser, RDFS.COMMENT, null); try { while (stmts.hasNext()) { Statement st = stmts.next(); add(regUser, st.getPredicate(), st.getObject(), con); } } finally { stmts.close(); } stmts = con.getStatements(invitedUser, DCTERMS.CREATED, null); try { while (stmts.hasNext()) { Statement st = stmts.next(); add(regUser, st.getPredicate(), st.getObject(), con); } } finally { stmts.close(); } stmts = con.getStatements(invitedUser, vf.createURI(FOAF_DEPICTION), null); try { while (stmts.hasNext()) { Statement st = stmts.next(); add(regUser, st.getPredicate(), st.getObject(), con); } } finally { stmts.close(); } if (fullname == null) { stmts = con.getStatements(invitedUser, RDFS.LABEL, null); try { while (stmts.hasNext()) { Value label = stmts.next().getObject(); if (!con.hasStatement(regUser, RDFS.LABEL, label)) { con.remove(regUser, RDFS.LABEL, null); con.add(regUser, RDFS.LABEL, label); } } } finally { stmts.close(); } } else { Literal label = vf.createLiteral(fullname); if (!con.hasStatement(regUser, RDFS.LABEL, label)) { con.remove(regUser, RDFS.LABEL, null); con.add(regUser, RDFS.LABEL, label); } } URI hasEmail = vf.createURI(EMAIL); if (email == null) { stmts = con.getStatements(invitedUser, hasEmail, null); try { while (stmts.hasNext()) { Value obj = stmts.next().getObject(); if (!con.hasStatement(regUser, hasEmail, obj)) { con.remove(regUser, hasEmail, null); con.add(regUser, hasEmail, obj); } } } finally { stmts.close(); } } else { Literal mailto = vf.createLiteral(email); if (!con.hasStatement(regUser, hasEmail, mailto)) { con.remove(regUser, hasEmail, null); con.add(regUser, hasEmail, mailto); } } add(regUser, RDF.TYPE, vf.createURI(PARTY), con); add(regUser, RDF.TYPE, vf.createURI(USER), con); add(regUser, vf.createURI(DERIVED_FROM), invitedUser, con); add(regUser, DCTERMS.MODIFIED, vf.createLiteral(now()), con); getManager().registered(invitedUser, regUser, con); con.remove(invitedUser, null, null); con.commit(); } private XMLGregorianCalendar now() { try { TimeZone utc = TimeZone.getTimeZone("UTC"); DatatypeFactory df = DatatypeFactory.newInstance(); return df.newXMLGregorianCalendar(new GregorianCalendar(utc)); } catch (DatatypeConfigurationException e) { throw new AssertionError(e); } } private void moveTo(URI link, Statement st, ObjectConnection con) throws RepositoryException { URI pred = st.getPredicate(); if (RDF.NAMESPACE.equals(pred.getNamespace())) return; if (PROV.equals(pred.getNamespace())) return; Resource subj = st.getSubject(); con.remove(subj, pred, st.getObject()); if (HAS_COMPONENT.equals(pred.stringValue())) return; add(subj, pred, link, con); } private void add(Resource subj, URI pred, Value obj, ObjectConnection con) throws RepositoryException { if (con.hasStatement(subj, pred, obj)) return; con.add(subj, pred, obj); } private String hash(String text, String algorithm) throws NoSuchAlgorithmException { byte[] hash = MessageDigest.getInstance(algorithm).digest(text.getBytes()); BigInteger bi = new BigInteger(1, hash); String result = bi.toString(16); if (result.length() % 2 != 0) { return "0" + result; } return result; } private Map<String, String> keyByName(String cookie) { Map<String, String> map = new LinkedHashMap<String, String>(); if (cookie != null) { for (String pair : cookie.split("&")) { int idx = pair.indexOf('='); if (idx > 0) { String name = decode(pair.substring(0, idx)); String value = decode(pair.substring(idx + 1)); map.put(name, value); } } } return map; } }