001/* 002 * Copyright (c) 2011-2017 Nexmo Inc 003 * 004 * Permission is hereby granted, free of charge, to any person obtaining a copy 005 * of this software and associated documentation files (the "Software"), to deal 006 * in the Software without restriction, including without limitation the rights 007 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 008 * copies of the Software, and to permit persons to whom the Software is 009 * furnished to do so, subject to the following conditions: 010 * 011 * The above copyright notice and this permission notice shall be included in 012 * all copies or substantial portions of the Software. 013 * 014 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 015 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 016 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 017 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 018 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 019 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 020 * THE SOFTWARE. 021 */ 022package com.nexmo.client.auth; 023 024import com.auth0.jwt.Algorithm; 025import com.auth0.jwt.JWTSigner; 026import com.nexmo.client.NexmoUnexpectedException; 027import org.apache.http.client.methods.RequestBuilder; 028 029import javax.xml.bind.DatatypeConverter; 030import java.io.IOException; 031import java.io.UnsupportedEncodingException; 032import java.nio.file.Files; 033import java.nio.file.Path; 034import java.security.InvalidKeyException; 035import java.security.KeyFactory; 036import java.security.NoSuchAlgorithmException; 037import java.security.PrivateKey; 038import java.security.spec.InvalidKeySpecException; 039import java.security.spec.PKCS8EncodedKeySpec; 040import java.util.HashMap; 041import java.util.Map; 042import java.util.UUID; 043import java.util.regex.Matcher; 044import java.util.regex.Pattern; 045 046public class JWTAuthMethod extends AbstractAuthMethod { 047 private static final Pattern pemPattern = Pattern.compile( 048 "-----BEGIN PRIVATE KEY-----" + // File header 049 "(.*\\n)" + // Key data 050 "-----END PRIVATE KEY-----" + // File footer 051 "\\n?", // Optional trailing line break 052 Pattern.MULTILINE | Pattern.DOTALL); 053 public final int SORT_KEY = 10; 054 private String applicationId; 055 private JWTSigner signer; 056 057 public JWTAuthMethod(final String applicationId, final byte[] privateKey) 058 throws NoSuchAlgorithmException, InvalidKeyException, InvalidKeySpecException { 059 this.applicationId = applicationId; 060 061 byte[] decodedPrivateKey = privateKey; 062 if (privateKey[0] == '-') { 063 decodedPrivateKey = decodePrivateKey(privateKey); 064 } 065 PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(decodedPrivateKey); 066 KeyFactory kf = KeyFactory.getInstance("RSA"); 067 PrivateKey key = kf.generatePrivate(spec); 068 this.signer = new JWTSigner(key); 069 } 070 071 public JWTAuthMethod(String applicationId, Path path) 072 throws NoSuchAlgorithmException, InvalidKeyException, InvalidKeySpecException, IOException { 073 this(applicationId, Files.readAllBytes(path)); 074 } 075 076 public static String constructJTI() { 077 return UUID.randomUUID().toString(); 078 } 079 080 protected byte[] decodePrivateKey(byte[] data) throws InvalidKeyException { 081 try { 082 String s = new String(data, "UTF-8"); 083 Matcher extracter = pemPattern.matcher(s); 084 if (extracter.matches()) { 085 String pemBody = extracter.group(1); 086 return DatatypeConverter.parseBase64Binary(pemBody); 087 } else { 088 throw new InvalidKeyException("Private key should be provided in PEM format!"); 089 } 090 } catch (UnsupportedEncodingException exc) { 091 // This should never happen. 092 throw new NexmoUnexpectedException("UTF-8 is an unsupported encoding in this JVM", exc); 093 } 094 } 095 096 @Override 097 public RequestBuilder apply(RequestBuilder request) { 098 String token = this.constructToken( 099 System.currentTimeMillis() / 1000L, 100 constructJTI()); 101 request.setHeader("Authorization", "Bearer " + token); 102 return request; 103 } 104 105 public String constructToken(long iat, String jti) { 106 Map<String, Object> claims = new HashMap<>(); 107 claims.put("iat", iat); 108 claims.put("application_id", this.applicationId); 109 claims.put("jti", jti); 110 111 JWTSigner.Options options = new JWTSigner.Options() 112 .setAlgorithm(Algorithm.RS256); 113 String signed = this.signer.sign(claims, options); 114 115 return signed; 116 } 117 118 @Override 119 public int getSortKey() { 120 return this.SORT_KEY; 121 } 122}