added resource registration endpoint with basic functionality and specialized tokens

pull/604/head
Justin Richer 11 years ago
parent 0775785ce3
commit c34357a433

@ -32,6 +32,7 @@ public interface SystemScopeService {
public static final String OFFLINE_ACCESS = "offline_access";
public static final String ID_TOKEN_SCOPE = "id-token";
public static final String REGISTRATION_TOKEN_SCOPE = "registration-token";
public static final String RESOURCE_TOKEN_SCOPE = "resource-token";
public Set<SystemScope> getAll();

@ -55,4 +55,12 @@ public interface OIDCTokenService {
*/
public OAuth2AccessTokenEntity createRegistrationAccessToken(ClientDetailsEntity client);
/**
* Create a resource access token for the given client (protected resource).
*
* @param client
* @return
*/
public OAuth2AccessTokenEntity createResourceAccessToken(ClientDetailsEntity client);
}

@ -105,7 +105,7 @@ var ResRegRootView = Backbone.View.extend({
var clientId = $('#clientId').val();
var token = $('#regtoken').val();
var client = new DynRegClient({
var client = new ResRegClient({
client_id: clientId,
registration_access_token: token
});
@ -185,7 +185,7 @@ var ResRegEditView = Backbone.View.extend({
this.model.destroy({
success:function () {
self.remove();
app.navigate('dev/dynreg', {trigger: true});
app.navigate('dev/resource', {trigger: true});
},
error:function (error, response) {
console.log("An error occurred when deleting a client");
@ -299,10 +299,8 @@ var ResRegEditView = Backbone.View.extend({
var attrs = {
client_name:$('#clientName input').val(),
redirect_uris: this.redirectUrisCollection.pluck("item"),
client_description:$('#clientDescription textarea').val(),
logo_uri:$('#logoUri input').val(),
grant_types: grantTypes,
scope: scopes,
tos_uri: $('#tosUri input').val(),
@ -310,25 +308,8 @@ var ResRegEditView = Backbone.View.extend({
client_uri: $('#clientUri input').val(),
application_type: $('#applicationType input').filter(':checked').val(),
jwks_uri: $('#jwksUri input').val(),
subject_type: $('#subjectType input').filter(':checked').val(),
token_endpoint_auth_method: $('#tokenEndpointAuthMethod input').filter(':checked').val(),
response_types: responseTypes,
sector_identifier_uri: $('#sectorIdentifierUri input').val(),
initiate_login_uri: $('#initiateLoginUri input').val(),
post_logout_redirect_uri: $('#postLogoutRedirectUri input').val(),
reuse_refresh_token: $('#reuseRefreshToken').is(':checked'),
require_auth_time: $('#requireAuthTime input').is(':checked'),
default_max_age: parseInt($('#defaultMaxAge input').val()),
contacts: contacts,
request_uris: this.requestUrisCollection.pluck('item'),
default_acr_values: this.defaultAcrValuesCollection.pluck('item'),
request_object_signing_alg: this.defaultToNull($('#requestObjectSigningAlg select').val()),
userinfo_signed_response_alg: this.defaultToNull($('#userInfoSignedResponseAlg select').val()),
userinfo_encrypted_response_alg: this.defaultToNull($('#userInfoEncryptedResponseAlg select').val()),
userinfo_encrypted_response_enc: this.defaultToNull($('#userInfoEncryptedResponseEnc select').val()),
id_token_signed_response_alg: this.defaultToNull($('#idTokenSignedResponseAlg select').val()),
id_token_encrypted_response_alg: this.defaultToNull($('#idTokenEncryptedResponseAlg select').val()),
id_token_encrypted_response_enc: this.defaultToNull($('#idTokenEncryptedResponseEnc select').val()),
token_endpoint_auth_signing_alg: this.defaultToNull($('#tokenEndpointAuthSigningAlg select').val())
};
@ -343,9 +324,9 @@ var ResRegEditView = Backbone.View.extend({
this.model.save(attrs, {
success:function () {
// switch to an "edit" view
app.navigate('dev/dynreg/edit', {trigger: true});
app.navigate('dev/resource/edit', {trigger: true});
_self.remove();
var view = new DynRegEditView({model: _self.model, systemScopeList: _self.options.systemScopeList});
var view = new ResRegEditView({model: _self.model, systemScopeList: _self.options.systemScopeList});
view.load(function() {
// reload

@ -212,7 +212,6 @@ public class DefaultOIDCTokenService implements OIDCTokenService {
claims.setExpirationTime(token.getExpiration());
claims.setJWTID(UUID.randomUUID().toString()); // set a random NONCE in the middle of it
// TODO: use client's default signing algorithm
JWSAlgorithm signingAlg = jwtService.getDefaultSigningAlgorithm();
SignedJWT signed = new SignedJWT(new JWSHeader(signingAlg), claims);
@ -221,7 +220,47 @@ public class DefaultOIDCTokenService implements OIDCTokenService {
token.setJwt(signed);
return token;
}
/**
* @param client
* @return
* @throws AuthenticationException
*/
@Override
public OAuth2AccessTokenEntity createResourceAccessToken(ClientDetailsEntity client) {
Map<String, String> authorizationParameters = Maps.newHashMap();
OAuth2Request clientAuth = new OAuth2Request(authorizationParameters, client.getClientId(),
Sets.newHashSet(new SimpleGrantedAuthority("ROLE_CLIENT")), true,
Sets.newHashSet(SystemScopeService.RESOURCE_TOKEN_SCOPE), null, null, null, null);
OAuth2Authentication authentication = new OAuth2Authentication(clientAuth, null);
OAuth2AccessTokenEntity token = new OAuth2AccessTokenEntity();
token.setClient(client);
token.setScope(Sets.newHashSet(SystemScopeService.RESOURCE_TOKEN_SCOPE));
AuthenticationHolderEntity authHolder = new AuthenticationHolderEntity();
authHolder.setAuthentication(authentication);
authHolder = authenticationHolderRepository.save(authHolder);
token.setAuthenticationHolder(authHolder);
JWTClaimsSet claims = new JWTClaimsSet();
claims.setAudience(Lists.newArrayList(client.getClientId()));
claims.setIssuer(configBean.getIssuer());
claims.setIssueTime(new Date());
claims.setExpirationTime(token.getExpiration());
claims.setJWTID(UUID.randomUUID().toString()); // set a random NONCE in the middle of it
JWSAlgorithm signingAlg = jwtService.getDefaultSigningAlgorithm();
SignedJWT signed = new SignedJWT(new JWSHeader(signingAlg), claims);
jwtService.signJwt(signed);
token.setJwt(signed);
return token;
}
/**

@ -177,6 +177,9 @@ public class ClientDynamicRegistrationEndpoint {
// this client has been dynamically registered (obviously)
newClient.setDynamicallyRegistered(true);
// this client can't do token introspection
newClient.setAllowIntrospection(false);
// now save it
try {
@ -285,7 +288,7 @@ public class ClientDynamicRegistrationEndpoint {
newClient.setIdTokenValiditySeconds(oldClient.getIdTokenValiditySeconds());
newClient.setRefreshTokenValiditySeconds(oldClient.getRefreshTokenValiditySeconds());
newClient.setDynamicallyRegistered(true); // it's still dynamically registered
newClient.setAllowIntrospection(oldClient.isAllowIntrospection());
newClient.setAllowIntrospection(false); // dynamically registered clients can't do introspection -- use the resource registration instead
newClient.setAuthorities(oldClient.getAuthorities());
newClient.setClientDescription(oldClient.getClientDescription());
newClient.setCreatedAt(oldClient.getCreatedAt());

@ -0,0 +1,359 @@
/*******************************************************************************
* Copyright 2014 The MITRE Corporation
* and the MIT Kerberos and Internet Trust Consortium
*
* 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 org.mitre.openid.connect.web;
import java.io.UnsupportedEncodingException;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import org.mitre.jwt.signer.service.JwtSigningAndValidationService;
import org.mitre.oauth2.model.ClientDetailsEntity;
import org.mitre.oauth2.model.ClientDetailsEntity.AuthMethod;
import org.mitre.oauth2.model.OAuth2AccessTokenEntity;
import org.mitre.oauth2.model.RegisteredClient;
import org.mitre.oauth2.model.SystemScope;
import org.mitre.oauth2.service.ClientDetailsEntityService;
import org.mitre.oauth2.service.OAuth2TokenEntityService;
import org.mitre.oauth2.service.SystemScopeService;
import org.mitre.openid.connect.ClientDetailsEntityJsonProcessor;
import org.mitre.openid.connect.config.ConfigurationPropertiesBean;
import org.mitre.openid.connect.service.BlacklistedSiteService;
import org.mitre.openid.connect.service.OIDCTokenService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.authentication.OAuth2AuthenticationDetails;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.util.UriUtils;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Sets;
@Controller
@RequestMapping(value = "resource")
public class ProtectedResourceRegistrationEndpoint {
@Autowired
private ClientDetailsEntityService clientService;
@Autowired
private OAuth2TokenEntityService tokenService;
@Autowired
private JwtSigningAndValidationService jwtService;
@Autowired
private SystemScopeService scopeService;
@Autowired
private BlacklistedSiteService blacklistService;
@Autowired
private ConfigurationPropertiesBean config;
@Autowired
private OIDCTokenService connectTokenService;
private static Logger logger = LoggerFactory.getLogger(ProtectedResourceRegistrationEndpoint.class);
/**
* Create a new Client, issue a client ID, and create a registration access token.
* @param jsonString
* @param m
* @param p
* @return
*/
@RequestMapping(method = RequestMethod.POST, consumes = "application/json", produces = "application/json")
public String registerNewProtectedResource(@RequestBody String jsonString, Model m) {
ClientDetailsEntity newClient = ClientDetailsEntityJsonProcessor.parse(jsonString);
if (newClient != null) {
// it parsed!
//
// Now do some post-processing consistency checks on it
//
// clear out any spurious id/secret (clients don't get to pick)
newClient.setClientId(null);
newClient.setClientSecret(null);
// set of scopes that are OK for clients to dynamically register for
Set<SystemScope> dynScopes = scopeService.getDynReg();
// scopes that the client is asking for
Set<SystemScope> requestedScopes = scopeService.fromStrings(newClient.getScope());
// the scopes that the client can have must be a subset of the dynamically allowed scopes
Set<SystemScope> allowedScopes = Sets.intersection(dynScopes, requestedScopes);
// if the client didn't ask for any, give them the defaults
if (allowedScopes == null || allowedScopes.isEmpty()) {
allowedScopes = scopeService.getDefaults();
}
newClient.setScope(scopeService.toStrings(allowedScopes));
// no grant types are allowed
newClient.setGrantTypes(new HashSet<String>());
newClient.setResponseTypes(new HashSet<String>());
newClient.setRedirectUris(new HashSet<String>());
if (newClient.getTokenEndpointAuthMethod() == null) {
newClient.setTokenEndpointAuthMethod(AuthMethod.SECRET_BASIC);
}
if (newClient.getTokenEndpointAuthMethod() == AuthMethod.SECRET_BASIC ||
newClient.getTokenEndpointAuthMethod() == AuthMethod.SECRET_JWT ||
newClient.getTokenEndpointAuthMethod() == AuthMethod.SECRET_POST) {
// we need to generate a secret
newClient = clientService.generateClientSecret(newClient);
}
// don't issue tokens to this client
newClient.setAccessTokenValiditySeconds(0);
newClient.setIdTokenValiditySeconds(0);
newClient.setRefreshTokenValiditySeconds(0);
// clear out unused fields
newClient.setDefaultACRvalues(new HashSet<String>());
newClient.setDefaultMaxAge(null);
newClient.setIdTokenEncryptedResponseAlg(null);
newClient.setIdTokenEncryptedResponseEnc(null);
newClient.setIdTokenSignedResponseAlg(null);
newClient.setInitiateLoginUri(null);
newClient.setPostLogoutRedirectUri(null);
newClient.setRequestObjectSigningAlg(null);
newClient.setRequireAuthTime(null);
newClient.setReuseRefreshToken(false);
newClient.setSectorIdentifierUri(null);
newClient.setSubjectType(null);
newClient.setUserInfoEncryptedResponseAlg(null);
newClient.setUserInfoEncryptedResponseEnc(null);
newClient.setUserInfoSignedResponseAlg(null);
// this client has been dynamically registered (obviously)
newClient.setDynamicallyRegistered(true);
// this client has access to the introspection endpoint
newClient.setAllowIntrospection(true);
// now save it
try {
ClientDetailsEntity savedClient = clientService.saveNewClient(newClient);
// generate the registration access token
OAuth2AccessTokenEntity token = connectTokenService.createResourceAccessToken(savedClient);
tokenService.saveAccessToken(token);
// send it all out to the view
RegisteredClient registered = new RegisteredClient(savedClient, token.getValue(), config.getIssuer() + "resource/" + UriUtils.encodePathSegment(savedClient.getClientId(), "UTF-8"));
m.addAttribute("client", registered);
m.addAttribute("code", HttpStatus.CREATED); // http 201
return "clientInformationResponseView";
} catch (UnsupportedEncodingException e) {
logger.error("Unsupported encoding", e);
m.addAttribute("code", HttpStatus.INTERNAL_SERVER_ERROR);
return "httpCodeView";
} catch (IllegalArgumentException e) {
logger.error("Couldn't save client", e);
m.addAttribute("code", HttpStatus.BAD_REQUEST);
return "httpCodeView";
}
} else {
// didn't parse, this is a bad request
logger.error("registerNewClient failed; submitted JSON is malformed");
m.addAttribute("code", HttpStatus.BAD_REQUEST); // http 400
return "httpCodeView";
}
}
/**
* Get the meta information for a client.
* @param clientId
* @param m
* @param auth
* @return
*/
@PreAuthorize("hasRole('ROLE_CLIENT') and #oauth2.hasScope('" + SystemScopeService.RESOURCE_TOKEN_SCOPE + "')")
@RequestMapping(value = "/{id}", method = RequestMethod.GET, produces = "application/json")
public String readResourceConfiguration(@PathVariable("id") String clientId, Model m, OAuth2Authentication auth) {
ClientDetailsEntity client = clientService.loadClientByClientId(clientId);
if (client != null && client.getClientId().equals(auth.getOAuth2Request().getClientId())) {
// we return the token that we got in
OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails) auth.getDetails();
OAuth2AccessTokenEntity token = tokenService.readAccessToken(details.getTokenValue());
try {
RegisteredClient registered = new RegisteredClient(client, token.getValue(), config.getIssuer() + "resource/" + UriUtils.encodePathSegment(client.getClientId(), "UTF-8"));
// send it all out to the view
m.addAttribute("client", registered);
m.addAttribute("code", HttpStatus.OK); // http 200
return "clientInformationResponseView";
} catch (UnsupportedEncodingException e) {
logger.error("Unsupported encoding", e);
m.addAttribute("code", HttpStatus.INTERNAL_SERVER_ERROR);
return "httpCodeView";
}
} else {
// client mismatch
logger.error("readResourceConfiguration failed, client ID mismatch: "
+ clientId + " and " + auth.getOAuth2Request().getClientId() + " do not match.");
m.addAttribute("code", HttpStatus.FORBIDDEN); // http 403
return "httpCodeView";
}
}
/**
* Update the metainformation for a given client.
* @param clientId
* @param jsonString
* @param m
* @param auth
* @return
*/
@PreAuthorize("hasRole('ROLE_CLIENT') and #oauth2.hasScope('" + SystemScopeService.RESOURCE_TOKEN_SCOPE + "')")
@RequestMapping(value = "/{id}", method = RequestMethod.PUT, produces = "application/json", consumes = "application/json")
public String updateProtectedResource(@PathVariable("id") String clientId, @RequestBody String jsonString, Model m, OAuth2Authentication auth) {
ClientDetailsEntity newClient = ClientDetailsEntityJsonProcessor.parse(jsonString);
ClientDetailsEntity oldClient = clientService.loadClientByClientId(clientId);
if (newClient != null && oldClient != null // we have an existing client and the new one parsed
&& oldClient.getClientId().equals(auth.getOAuth2Request().getClientId()) // the client passed in the URI matches the one in the auth
&& oldClient.getClientId().equals(newClient.getClientId()) // the client passed in the body matches the one in the URI
) {
// a client can't ask to update its own client secret to any particular value
newClient.setClientSecret(oldClient.getClientSecret());
// we need to copy over all of the local and SECOAUTH fields
newClient.setAccessTokenValiditySeconds(oldClient.getAccessTokenValiditySeconds());
newClient.setIdTokenValiditySeconds(oldClient.getIdTokenValiditySeconds());
newClient.setRefreshTokenValiditySeconds(oldClient.getRefreshTokenValiditySeconds());
newClient.setDynamicallyRegistered(true); // it's still dynamically registered
newClient.setAllowIntrospection(oldClient.isAllowIntrospection());
newClient.setAuthorities(oldClient.getAuthorities());
newClient.setClientDescription(oldClient.getClientDescription());
newClient.setCreatedAt(oldClient.getCreatedAt());
newClient.setReuseRefreshToken(oldClient.isReuseRefreshToken());
// set of scopes that are OK for clients to dynamically register for
Set<SystemScope> dynScopes = scopeService.getDynReg();
// scopes that the client is asking for
Set<SystemScope> requestedScopes = scopeService.fromStrings(newClient.getScope());
// the scopes that the client can have must be a subset of the dynamically allowed scopes
Set<SystemScope> allowedScopes = Sets.intersection(dynScopes, requestedScopes);
// make sure that the client doesn't ask for scopes it can't have
newClient.setScope(scopeService.toStrings(allowedScopes));
try {
// save the client
ClientDetailsEntity savedClient = clientService.updateClient(oldClient, newClient);
// we return the token that we got in
// TODO: rotate this after some set amount of time
OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails) auth.getDetails();
OAuth2AccessTokenEntity token = tokenService.readAccessToken(details.getTokenValue());
RegisteredClient registered = new RegisteredClient(savedClient, token.getValue(), config.getIssuer() + "register/" + UriUtils.encodePathSegment(savedClient.getClientId(), "UTF-8"));
// send it all out to the view
m.addAttribute("client", registered);
m.addAttribute("code", HttpStatus.OK); // http 200
return "clientInformationResponseView";
} catch (IllegalArgumentException e) {
logger.error("Couldn't save client", e);
m.addAttribute("code", HttpStatus.BAD_REQUEST);
return "httpCodeView";
} catch (UnsupportedEncodingException e) {
logger.error("Unsupported encoding", e);
m.addAttribute("code", HttpStatus.INTERNAL_SERVER_ERROR);
return "httpCodeView";
}
} else {
// client mismatch
logger.error("readClientConfiguration failed, client ID mismatch: "
+ clientId + " and " + auth.getOAuth2Request().getClientId() + " do not match.");
m.addAttribute("code", HttpStatus.FORBIDDEN); // http 403
return "httpCodeView";
}
}
/**
* Delete the indicated client from the system.
* @param clientId
* @param m
* @param auth
* @return
*/
@PreAuthorize("hasRole('ROLE_CLIENT') and #oauth2.hasScope('" + SystemScopeService.RESOURCE_TOKEN_SCOPE + "')")
@RequestMapping(value = "/{id}", method = RequestMethod.DELETE, produces = "application/json")
public String deleteResource(@PathVariable("id") String clientId, Model m, OAuth2Authentication auth) {
ClientDetailsEntity client = clientService.loadClientByClientId(clientId);
if (client != null && client.getClientId().equals(auth.getOAuth2Request().getClientId())) {
clientService.deleteClient(client);
m.addAttribute("code", HttpStatus.NO_CONTENT); // http 204
return "httpCodeView";
} else {
// client mismatch
logger.error("readClientConfiguration failed, client ID mismatch: "
+ clientId + " and " + auth.getOAuth2Request().getClientId() + " do not match.");
m.addAttribute("code", HttpStatus.FORBIDDEN); // http 403
return "httpCodeView";
}
}
}
Loading…
Cancel
Save