Created
September 12, 2013 18:55
-
-
Save rruhlen/6542239 to your computer and use it in GitHub Desktop.
Multipass SSO Java example for Desk.com
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.meetup.util.desk; | |
import org.joda.time.DateTime; | |
import org.joda.time.format.ISODateTimeFormat; | |
import com.google.common.collect.ImmutableMap; | |
import com.google.common.collect.ImmutableSet; | |
import com.meetup.util.MeetupLogger; | |
import com.meetup.base.util.U; | |
import com.meetup.base.util.api.RemoteApiUtil; | |
import com.meetup.base.util.api.RemoteApiUtil.JsonEncodingException; | |
import java.io.UnsupportedEncodingException; | |
import java.util.Arrays; | |
import java.util.Map; | |
import java.util.Set; | |
import java.security.Key; | |
import java.security.InvalidAlgorithmParameterException; | |
import java.security.InvalidKeyException; | |
import java.security.MessageDigest; | |
import java.security.NoSuchAlgorithmException; | |
import java.security.spec.KeySpec; | |
import javax.crypto.BadPaddingException; | |
import javax.crypto.Cipher; | |
import javax.crypto.Mac; | |
import javax.crypto.IllegalBlockSizeException; | |
import javax.crypto.NoSuchPaddingException; | |
import javax.crypto.spec.IvParameterSpec; | |
import javax.crypto.spec.SecretKeySpec; | |
import org.apache.commons.io.HexDump; | |
import org.apache.commons.codec.binary.Base64; | |
/** | |
* Signing utility for desk.com sso | |
* Result r = Multipass.signer( sitetoken, apitoken ) | |
* .uid( ... ) | |
* .customerName( ... ) | |
* .customerEmail( ... ) | |
* .expires( ... ) | |
* .sign(); | |
* String url = String.format( "host.com/path?multipass=%s&signature=%s", | |
* r.token() r.signature() ); | |
*/ | |
public class Multipass { | |
public static class Signer { | |
public static class Result { | |
private final String token; | |
private final String signature; | |
public Result( String token, String sig ) { | |
this.token = token; | |
this.signature = sig; | |
} | |
public String token() { | |
return this.token; | |
} | |
public String signature() { | |
return this.signature; | |
} | |
} | |
private static final MeetupLogger log = | |
MeetupLogger.getLogger( Multipass.class.getName() ); | |
private static final String SIGN_ALG = "HmacSHA1"; | |
private static final String TOKEN_ALG = "AES"; | |
// desk.com requires AES128-cbc with 16 byte blocks | |
// see http://docs.oracle.com/javase/7/docs/api/javax/crypto/Cipher.html | |
private static final String TOKEN_CIPHER = "AES/CBC/NoPadding"; | |
private static final String STR_ENCODING = "utf8"; | |
private static final Set<String> require = ImmutableSet.of( | |
"uid", "customer_name", "customer_email", "expires" | |
); | |
private static final String DESK_IV_SRC = "OpenSSL for Ruby"; | |
private static final int BLOCK_SIZE = DESK_IV_SRC.length(); | |
private final ImmutableMap.Builder params = ImmutableMap.builder(); | |
private final byte[] secretKey; | |
private final byte[] apiKey; | |
private final byte[] iv; | |
public Signer( String siteKey, String apiKey ) { | |
try { this.secretKey = sha1( bytes( apiKey + siteKey ), BLOCK_SIZE ); } | |
catch ( NoSuchAlgorithmException na ) { | |
throw new IllegalStateException( "This system does not support SHA-1 encryption", | |
na ); | |
} | |
this.apiKey = bytes( apiKey ); | |
this.iv = bytes( DESK_IV_SRC ); | |
} | |
public Signer uid( String id ) { | |
this.params.put( "uid", id ); | |
return this; | |
} | |
public Signer customerName( String n ) { | |
this.params.put( "customer_name", n ); | |
return this; | |
} | |
public Signer customerEmail( String e ) { | |
this.params.put( "customer_email", e ); | |
return this; | |
} | |
public Signer expires( DateTime e ) { | |
this.params.put( "expires", ISODateTimeFormat.ordinalDateTimeNoMillis().print( e ) ); | |
return this; | |
} | |
public Signer to( String to ) { | |
this.params.put( "to", to ); | |
return this; | |
} | |
public Signer custom( String key, String value ) { | |
this.params.put( "customer_custom_" + key, value ); | |
return this; | |
} | |
public Result sign() { | |
Map<String, String> ps = this.params.build(); | |
if ( validate( ps ) ) { | |
try { | |
String token = token(); | |
return new Result( token, sign( token ) ); | |
} | |
catch ( BadPaddingException pb ) { | |
pb.printStackTrace(); | |
log.error( "bad padding", pb ); | |
} | |
catch ( InvalidAlgorithmParameterException ip ) { | |
ip.printStackTrace(); | |
log.error( "invalid algo param", ip ); | |
} | |
catch ( IllegalBlockSizeException ib ) { | |
ib.printStackTrace(); | |
log.error( "illegal block size", ib ); | |
} | |
catch ( JsonEncodingException je ) { | |
je.printStackTrace(); | |
log.error( "json encoding exception", je ); | |
} | |
catch ( NoSuchAlgorithmException na ) { | |
na.printStackTrace(); | |
log.error( "no such alg ", na ); | |
} | |
catch ( NoSuchPaddingException np ) { | |
np.printStackTrace(); | |
log.error("no such padding", np ); | |
} | |
catch ( InvalidKeyException ik ) { | |
ik.printStackTrace(); | |
log.error( "invalid key", ik ); | |
} | |
} | |
else { | |
throw new IllegalStateException( "missing or invalid params: " + require ); | |
} | |
return null; | |
} | |
public String json() throws JsonEncodingException { | |
return RemoteApiUtil.jsonize( params.build() ).toString(); | |
} | |
private String sign( String token ) | |
throws NoSuchAlgorithmException, | |
InvalidKeyException { | |
Key key = new SecretKeySpec( apiKey, SIGN_ALG ); | |
Mac mac = Mac.getInstance( SIGN_ALG ); | |
mac.init( key ); | |
return base64( mac.doFinal( bytes( token ) ) ); | |
} | |
public String decrypt( String token ) { | |
try { | |
byte[] decoded = Base64.decodeBase64( bytes( unesc( token ) ) ); | |
Cipher cipher = Cipher.getInstance( TOKEN_CIPHER ); | |
cipher.init( Cipher.DECRYPT_MODE, | |
new SecretKeySpec( secretKey, TOKEN_ALG ), | |
new IvParameterSpec( iv ) ); | |
return new String( xorFirstBlock( cipher.doFinal( decoded ) ), | |
STR_ENCODING ); | |
} | |
catch ( Exception e ) { | |
log.error( "error decrypting token " + token, e ); | |
throw new RuntimeException( e ); | |
} | |
} | |
private String token() | |
throws BadPaddingException, | |
NoSuchAlgorithmException, | |
NoSuchPaddingException, | |
InvalidAlgorithmParameterException, | |
IllegalBlockSizeException, | |
InvalidKeyException, | |
JsonEncodingException { | |
// cipher iv (Initialization Vector) | |
IvParameterSpec ivSpec = new IvParameterSpec( iv ); | |
// payload | |
byte[] payload = bytes( json() ); | |
int pad = BLOCK_SIZE - payload.length % BLOCK_SIZE; | |
byte[] padded = xorFirstBlock( pad == BLOCK_SIZE ? payload : padTo( payload, pad ) ); | |
// encrypt | |
Cipher cipher = Cipher.getInstance( TOKEN_CIPHER ); | |
cipher.init( Cipher.ENCRYPT_MODE, | |
new SecretKeySpec( secretKey, TOKEN_ALG ), | |
ivSpec ); | |
byte[] encr = cipher.doFinal( padded ); | |
// b64 encode and escape | |
return esc( base64( encr ) ); | |
} | |
private byte[] xorFirstBlock( byte[] bytes ) { | |
for ( int i = 0; i < iv.length; i++ ) { | |
bytes[i] = (byte) ( bytes[i] ^ iv[i] ); | |
} | |
return bytes; | |
} | |
private boolean validate( Map<String, String> ps ) { | |
for ( String r : require ) { | |
if ( U.isEmpty( ps.get( r ) ) ) { | |
return false; | |
} | |
} | |
return true; | |
} | |
// utils | |
private String esc( String s ) { | |
return s.replaceAll( "\\s+", "" ) | |
.replaceAll( "\\=+$", "" ) | |
.replaceAll( "\\+", "-" ) | |
.replaceAll( "\\/","_" ); | |
} | |
private String unesc( String s ) { | |
return s.replaceAll( "_", "/" ) | |
.replaceAll( "\\-", "+" ); | |
} | |
private byte[] padTo( byte[] bytes, int pad ) { | |
byte[] padded = Arrays.copyOf( bytes, bytes.length + pad ); | |
if ( pad > 0 ) { | |
for ( int i = bytes.length; i < padded.length; i ++ ) { | |
padded[i] = (byte) pad; | |
} | |
} | |
return padded; | |
} | |
private byte[] bytes( String str ) { | |
try { return str.getBytes( STR_ENCODING ); } | |
catch ( UnsupportedEncodingException e ) { | |
// not likely | |
log.error( "unsupported encoding " + STR_ENCODING, e ); | |
return null; | |
} | |
} | |
private byte[] sha1( byte[] bytes, int len ) throws NoSuchAlgorithmException { | |
return Arrays.copyOf( MessageDigest.getInstance( "SHA-1" ).digest( bytes ), len ); | |
} | |
private final String base64( byte[] bytes ) { | |
return Base64.encodeBase64String( bytes ); | |
} | |
} | |
/** factory for Signer */ | |
public static final Signer signer( String siteKey, String apiKey ) { | |
return new Signer( siteKey, apiKey ); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment