001/* 002 * Copyright 2012-2018 the original author or authors. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package org.springframework.boot.actuate.autoconfigure.cloudfoundry; 018 019import java.nio.charset.StandardCharsets; 020import java.util.List; 021import java.util.Map; 022 023import org.springframework.boot.actuate.autoconfigure.cloudfoundry.CloudFoundryAuthorizationException.Reason; 024import org.springframework.boot.json.JsonParserFactory; 025import org.springframework.util.Base64Utils; 026import org.springframework.util.StringUtils; 027 028/** 029 * The JSON web token provided with each request that originates from Cloud Foundry. 030 * 031 * @author Madhura Bhave 032 * @since 2.0.0 033 */ 034public class Token { 035 036 private final String encoded; 037 038 private final String signature; 039 040 private final Map<String, Object> header; 041 042 private final Map<String, Object> claims; 043 044 public Token(String encoded) { 045 this.encoded = encoded; 046 int firstPeriod = encoded.indexOf('.'); 047 int lastPeriod = encoded.lastIndexOf('.'); 048 if (firstPeriod <= 0 || lastPeriod <= firstPeriod) { 049 throw new CloudFoundryAuthorizationException(Reason.INVALID_TOKEN, 050 "JWT must have header, body and signature"); 051 } 052 this.header = parseJson(encoded.substring(0, firstPeriod)); 053 this.claims = parseJson(encoded.substring(firstPeriod + 1, lastPeriod)); 054 this.signature = encoded.substring(lastPeriod + 1); 055 if (!StringUtils.hasLength(this.signature)) { 056 throw new CloudFoundryAuthorizationException(Reason.INVALID_TOKEN, 057 "Token must have non-empty crypto segment"); 058 } 059 } 060 061 private Map<String, Object> parseJson(String base64) { 062 try { 063 byte[] bytes = Base64Utils.decodeFromUrlSafeString(base64); 064 return JsonParserFactory.getJsonParser() 065 .parseMap(new String(bytes, StandardCharsets.UTF_8)); 066 } 067 catch (RuntimeException ex) { 068 throw new CloudFoundryAuthorizationException(Reason.INVALID_TOKEN, 069 "Token could not be parsed", ex); 070 } 071 } 072 073 public byte[] getContent() { 074 return this.encoded.substring(0, this.encoded.lastIndexOf('.')).getBytes(); 075 } 076 077 public byte[] getSignature() { 078 return Base64Utils.decodeFromUrlSafeString(this.signature); 079 } 080 081 public String getSignatureAlgorithm() { 082 return getRequired(this.header, "alg", String.class); 083 } 084 085 public String getIssuer() { 086 return getRequired(this.claims, "iss", String.class); 087 } 088 089 public long getExpiry() { 090 return getRequired(this.claims, "exp", Integer.class).longValue(); 091 } 092 093 @SuppressWarnings("unchecked") 094 public List<String> getScope() { 095 return getRequired(this.claims, "scope", List.class); 096 } 097 098 public String getKeyId() { 099 return getRequired(this.header, "kid", String.class); 100 } 101 102 @SuppressWarnings("unchecked") 103 private <T> T getRequired(Map<String, Object> map, String key, Class<T> type) { 104 Object value = map.get(key); 105 if (value == null) { 106 throw new CloudFoundryAuthorizationException(Reason.INVALID_TOKEN, 107 "Unable to get value from key " + key); 108 } 109 if (!type.isInstance(value)) { 110 throw new CloudFoundryAuthorizationException(Reason.INVALID_TOKEN, 111 "Unexpected value type from key " + key + " value " + value); 112 } 113 return (T) value; 114 } 115 116 @Override 117 public String toString() { 118 return this.encoded; 119 } 120 121}