/**
* personium.io
* Copyright 2014 FUJITSU LIMITED
*
* 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.fujitsu.dc.core.auth;
import java.io.IOException;
import java.io.InputStream;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.Signature;
import java.security.SignatureException;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.RSAPublicKeySpec;
import org.apache.http.HttpResponse;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.cache.CachingHttpClient;
import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
import org.json.simple.parser.JSONParser;
import org.json.simple.parser.ParseException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.fujitsu.dc.common.utils.DcCoreUtils;
import com.fujitsu.dc.core.DcCoreAuthnException;
import com.fujitsu.dc.core.DcCoreException;
/**
* IdToken.
*/
public class IdToken {
static Logger log = LoggerFactory.getLogger(IdToken.class);
private String header;
private String payload;
private String signature;
/* header */
private String kid;
/* payload */
private String email;
private String issuer;
private String audience;
private Long exp;
private static final String GOOGLE_DISCOV_DOC_URL = "https://accounts.google.com/.well-known/openid-configuration";
private static final String ALG = "SHA256withRSA";
private static final String KID = "kid";
private static final String KTY = "kty";
private static final String ISS = "iss";
private static final String EML = "email";
private static final String AUD = "aud";
private static final String EXP = "exp";
private static final String N = "n";
private static final String E = "e";
private static final int SPLIT_TOKEN_NUM = 3;
private static final int VERIFY_WAIT = 60;
private static final int VERIFY_SECOND = 1000;
/**
* IdToken.
*/
public IdToken() {
}
/**
* IdToken.
* @param json JSON
*/
public IdToken(JSONObject json) {
this.setEmail((String) json.get("email"));
this.setIssuer((String) json.get("issuer"));
this.setAudience((String) json.get("audience"));
this.setExp((Long) json.get("exp"));
}
/**
* 最終検証結果を返す.
* @param null
* @retrun boolean
* @throws DcCoreAuthnException dcae
*/
public void verify() throws DcCoreAuthnException {
// expireしていないかチェック(60秒くらいは過ぎても良い)
boolean expired = (exp + VERIFY_WAIT) * VERIFY_SECOND < System.currentTimeMillis();
if (expired) {
throw DcCoreAuthnException.OIDC_EXPIRED_ID_TOKEN.params(exp);
}
// 署名検証
verifySignature();
}
/**
* 署名検証.
*
* @param null
* @return boolean
*/
private void verifySignature() {
RSAPublicKey rsaPubKey = this.getKey();
try {
Signature sig = Signature.getInstance(ALG);
sig.initVerify(rsaPubKey);
sig.update((this.getHeader() + "." + this.getPayload()).getBytes());
boolean verified = sig.verify(DcCoreUtils.decodeBase64Url(this.getSignature()));
if (!verified) {
// 署名検証結果、署名が不正であると認定
throw DcCoreAuthnException.OIDC_AUTHN_FAILED;
}
} catch (NoSuchAlgorithmException e) {
// 環境がおかしい以外でここには来ない
throw new RuntimeException(ALG + " not supported.", e);
} catch (InvalidKeyException e) {
// バグ以外でここには来ない
throw new RuntimeException(e);
} catch (SignatureException e) {
// IdTokenのSignatureがおかしい
// the passed-in signature is improperly encoded or of the wrong
// type,
// if this signature algorithm is unable to process the input data
// provided, etc.
throw DcCoreAuthnException.OIDC_INVALID_ID_TOKEN.params("ID Token sig value is invalid.");
}
}
/**
* 公開鍵情報から、IDTokenのkidにマッチする方で公開鍵を生成.
*
* @return RSAPublicKey 公開鍵
*/
private RSAPublicKey getKey() {
JSONArray jsonAry = getKeys();
for (int i = 0; i < jsonAry.size(); i++) {
JSONObject k = (JSONObject) jsonAry.get(i);
String compKid = (String) k.get(KID);
if (compKid.equals(this.getKid())) {
BigInteger n = new BigInteger(1, DcCoreUtils.decodeBase64Url((String) k.get(N)));
BigInteger e = new BigInteger(1, DcCoreUtils.decodeBase64Url((String) k.get(E)));
RSAPublicKeySpec rsaPubKey = new RSAPublicKeySpec(n, e);
try {
KeyFactory kf = KeyFactory.getInstance((String) k.get(KTY));
return (RSAPublicKey) kf.generatePublic(rsaPubKey);
} catch (NoSuchAlgorithmException e1) {
// ktyの値がRSA以外はサポートしない
throw DcCoreException.NetWork.UNEXPECTED_VALUE.params(KTY, "RSA").reason(e1);
} catch (InvalidKeySpecException e1) {
// バグ以外でここには来ない
throw new RuntimeException(e1);
}
}
}
// 該当するkidを持つ鍵情報が取れなかった場合
throw DcCoreAuthnException.OIDC_INVALID_ID_TOKEN.params("ID Token header value is invalid.");
}
/**
* IdToken の検証のためのパース処理.
*
* @param idTokenStr IDトークン
*
* @return IdToken idToken
*/
public static IdToken parse(String idTokenStr) {
IdToken ret = new IdToken();
String[] splitIdToken = idTokenStr.split("\\.");
if (splitIdToken.length != SPLIT_TOKEN_NUM) {
throw DcCoreAuthnException.OIDC_INVALID_ID_TOKEN.params("2 periods required.");
}
ret.header = splitIdToken[0];
ret.payload = splitIdToken[1];
ret.signature = splitIdToken[2];
try {
String headerDecoded = new String(DcCoreUtils.decodeBase64Url(ret.header), StandardCharsets.UTF_8);
String payloadDecoded = new String(DcCoreUtils.decodeBase64Url(ret.payload), StandardCharsets.UTF_8);
JSONObject header = (JSONObject) new JSONParser().parse(headerDecoded);
JSONObject payload = (JSONObject) new JSONParser().parse(payloadDecoded);
ret.kid = (String) header.get(KID);
ret.issuer = (String) payload.get(ISS);
ret.email = (String) payload.get(EML);
ret.audience = (String) payload.get(AUD);
ret.exp = (Long) payload.get(EXP);
} catch (ParseException e) {
// BASE64はOk.JSONのパースに失敗.
throw DcCoreAuthnException.OIDC_INVALID_ID_TOKEN
.params("Header and payload should be Base64 encoded JSON.");
} catch (Exception e) {
// BASE64が失敗.
throw DcCoreAuthnException.OIDC_INVALID_ID_TOKEN.params("Header and payload should be Base64 encoded.");
}
return ret;
}
private static String getJwksUri(String endpoint) {
return (String) getHttpJSON(endpoint).get("jwks_uri");
}
private static JSONArray getKeys() {
return (JSONArray) getHttpJSON(getJwksUri(GOOGLE_DISCOV_DOC_URL)).get("keys");
}
/**
* Cacheを聞かせるため、ClientをStaticとする. たかだか限定されたURLのbodyを保存するのみであり、
* 最大キャッシュサイズはCacheConfigクラスで定義された16kbyte程度である. そのため、Staticで持つこととした.
*/
private static HttpClient httpClient = new CachingHttpClient();
/**
* HTTPでJSONオブジェクトを取得する処理. Cacheが利用可能であればその値を用いる.
*
* @param url URL
* @return JSONObject
*/
public static JSONObject getHttpJSON(String url) {
HttpGet get = new HttpGet(url);
int status = 0;
try {
HttpResponse res = httpClient.execute(get);
InputStream is = res.getEntity().getContent();
status = res.getStatusLine().getStatusCode();
String body = DcCoreUtils.readInputStreamAsString(is);
JSONObject jsonObj = (JSONObject) new JSONParser().parse(body);
return jsonObj;
} catch (ClientProtocolException e) {
// HTTPのプロトコル違反
throw DcCoreException.NetWork.UNEXPECTED_RESPONSE.params(url, "proper HTTP response", status).reason(e);
} catch (IOException e) {
// サーバーに接続できない場合に発生
throw DcCoreException.NetWork.HTTP_REQUEST_FAILED.params(HttpGet.METHOD_NAME, url).reason(e);
} catch (ParseException e) {
// JSONでないものを返してきた
throw DcCoreException.NetWork.UNEXPECTED_RESPONSE.params(url, "JSON", status).reason(e);
}
}
/**
* getHeader.
* @return header
*/
public String getHeader() {
return header;
}
/**
* setHeader.
* @param header HEADER
*/
public void setHeader(String header) {
this.header = header;
}
/**
* getPayload.
* @return payload
*/
public String getPayload() {
return payload;
}
/**
* setPayload.
* @param payload PAYLOAD
*/
public void setPayload(String payload) {
this.payload = payload;
}
/**
* getSignature.
* @return signature
*/
public String getSignature() {
return signature;
}
/**
* setSignature.
* @param signature SIGATURE
*/
public void setSignature(String signature) {
this.signature = signature;
}
/**
* getKid.
* @return kid
*/
public String getKid() {
return kid;
}
/**
* setKid.
* @param kid KID
*/
public void setKid(String kid) {
this.kid = kid;
}
/**
* getEmail.
* @return email
*/
public String getEmail() {
return email;
}
/**
* setEmail.
* @param email E-MAIL
*/
public void setEmail(String email) {
this.email = email;
}
/**
* getIssuer.
* @return issuer
*/
public String getIssuer() {
return issuer;
}
/**
* setIssuer.
* @param issuer ISSUER
*/
public void setIssuer(String issuer) {
this.issuer = issuer;
}
/**
* getAudience.
* @return audience
*/
public String getAudience() {
return audience;
}
/**
* setAudience.
* @param audience audience
*/
public void setAudience(String audience) {
this.audience = audience;
}
/**
* getExp.
* @return exp
*/
public Long getExp() {
return exp;
}
/**
* setExp.
* @param exp exp
*/
public void setExp(Long exp) {
this.exp = exp;
}
}