allow dynamic registration of clients

pull/1536/head
Justin Richer 2020-06-03 16:59:45 -04:00
parent 80eb57402c
commit 687e63fe05
1 changed files with 97 additions and 26 deletions

View File

@ -38,6 +38,7 @@ import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import com.google.common.collect.ImmutableMap;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
@ -108,12 +109,7 @@ public class TxEndpoint {
// otherwise we build one from the parts
tx = new TxEntity();
// client ID is passed in as the handle for the key object
// we don't support ephemeral keys (yet)
String clientId = json.get("keys").getAsString();
// first, load the client
ClientDetailsEntity client = clientService.loadClientByClientId(clientId);
ClientDetailsEntity client = loadOrRegisterClient(json);
if (client == null) {
m.addAttribute(JsonErrorView.ERROR, "unknown_key");
@ -169,11 +165,14 @@ public class TxEndpoint {
switch (tx.getStatus()) {
case NEW:
// now make sure the client is asking for scopes that it's allowed to
if (!scopeService.scopesMatch(tx.getClient().getScope(), tx.getScope())) {
m.addAttribute(JsonErrorView.ERROR, "resource_not_allowed");
m.addAttribute(JsonErrorView.ERROR_MESSAGE, "The client requested resources it does not have access to.");
m.addAttribute(HttpCodeView.CODE, HttpStatus.BAD_REQUEST);
return JsonErrorView.VIEWNAME;
// note: if the client has no scopes registered, it can ask for anything
if (!tx.getClient().getScope().isEmpty()) {
if (!scopeService.scopesMatch(tx.getClient().getScope(), tx.getScope())) {
m.addAttribute(JsonErrorView.ERROR, "resource_not_allowed");
m.addAttribute(JsonErrorView.ERROR_MESSAGE, "The client requested resources it does not have access to.");
m.addAttribute(HttpCodeView.CODE, HttpStatus.BAD_REQUEST);
return JsonErrorView.VIEWNAME;
}
}
OAuth2Request o2r = new OAuth2Request(
@ -215,22 +214,24 @@ public class TxEndpoint {
String callbackString = callback.get("uri").getAsString();
Path callbackPath = Paths.get(URI.create(callbackString).getPath());
// we do sub-path matching for the callback
// FIXME: this is a really simplistic filter that definitely has holes in it
boolean callbackMatches = tx.getClient().getRedirectUris().stream()
.filter(s -> callbackString.startsWith(s))
.map(URI::create)
.map(URI::getPath)
.map(Paths::get)
.anyMatch(path ->
callbackPath.startsWith(path)
);
if (!tx.getClient().getRedirectUris().isEmpty()) {
// we do sub-path matching for the callback
// FIXME: this is a really simplistic filter that definitely has holes in it
boolean callbackMatches = tx.getClient().getRedirectUris().stream()
.filter(s -> callbackString.startsWith(s))
.map(URI::create)
.map(URI::getPath)
.map(Paths::get)
.anyMatch(path ->
callbackPath.startsWith(path)
);
if (!callbackMatches) {
m.addAttribute(JsonErrorView.ERROR, "invalid_callback_uri");
m.addAttribute(JsonErrorView.ERROR_MESSAGE, "The client presented a callback URI that did not match one registered.");
m.addAttribute(HttpCodeView.CODE, HttpStatus.BAD_REQUEST);
return JsonErrorView.VIEWNAME;
if (!callbackMatches) {
m.addAttribute(JsonErrorView.ERROR, "invalid_callback_uri");
m.addAttribute(JsonErrorView.ERROR_MESSAGE, "The client presented a callback URI that did not match one registered.");
m.addAttribute(HttpCodeView.CODE, HttpStatus.BAD_REQUEST);
return JsonErrorView.VIEWNAME;
}
}
tx.setCallbackUri(callbackString);
@ -258,6 +259,14 @@ public class TxEndpoint {
h.put("presentation", "bearer");
map.put("handle", h);
if (tx.getClient().isDynamicallyRegistered()) {
Map<String, String> kh = ImmutableMap.of(
"value", tx.getClient().getClientId(),
"presentation", "bearer"
);
map.put("key_handle", kh);
}
txService.save(tx);
m.addAttribute(JsonEntityView.ENTITY, map);
@ -320,6 +329,68 @@ public class TxEndpoint {
}
private ClientDetailsEntity loadOrRegisterClient(JsonObject json) {
if (json.get("keys").isJsonObject()) {
// dynamically register the client
if (!json.get("keys").getAsJsonObject().has("jwk")) {
// we can only do a JWKS-based key proof
return null;
}
if (json.get("keys").getAsJsonObject().has("proof")
&& json.get("keys").getAsJsonObject().get("proof").getAsString().equals("jwsd")) {
try {
String jwkString = json.get("keys").getAsJsonObject().get("jwk").toString();
JWK jwk = JWK.parse(jwkString); // we have to round-trip this to get into the native object format for Nimbus
// TODO: see if we can figure out how to look up the client by its key value
//ClientDetailsEntity client = clientService.findClientByPublicKey(jwk);
// we create a new client and register it with the given key
ClientDetailsEntity client = new ClientDetailsEntity();
client.setDynamicallyRegistered(true);
client.setJwks(new JWKSet(jwk));
if (json.has("display") && json.get("display").isJsonObject()) {
JsonObject display = json.get("display").getAsJsonObject();
if (display.has("name")) {
client.setClientName(display.get("name").getAsString());
}
if (display.has("uri")) {
client.setClientUri(display.get("uri").getAsString());
}
if (display.has("logo_uri")) {
client.setLogoUri(display.get("logo_uri").getAsString());
}
}
ClientDetailsEntity saved = clientService.saveNewClient(client);
return saved;
} catch (ParseException e) {
return null;
}
} else {
// unsupported proof type
return null;
}
} else {
// client ID is passed in as the handle for the key object
String clientId = json.get("keys").getAsString();
// first, load the client
ClientDetailsEntity client = clientService.loadClientByClientId(clientId);
return client;
}
}
private void checkDetachedJws(String jwsd, String requestBody, JWK clientKey) {
try {