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}