diff --git a/uma-server-webapp/src/main/resources/db/clients.sql b/uma-server-webapp/src/main/resources/db/clients.sql index cb8a6c236..7921e54b9 100644 --- a/uma-server-webapp/src/main/resources/db/clients.sql +++ b/uma-server-webapp/src/main/resources/db/clients.sql @@ -22,8 +22,7 @@ INSERT INTO client_scope_TEMP (owner_id, scope) VALUES ('client', 'address'), ('client', 'phone'), ('client', 'offline_access'), - ('rs', 'uma_protection'), - ('c', 'uma_authorization'); + ('rs', 'uma_protection'); INSERT INTO client_redirect_uri_TEMP (owner_id, redirect_uri) VALUES ('client', 'http://localhost/'), @@ -36,8 +35,7 @@ INSERT INTO client_grant_type_TEMP (owner_id, grant_type) VALUES ('client', 'refresh_token'), ('rs', 'authorization_code'), ('rs', 'implicit'), - ('c', 'authorization_code'), - ('c', 'implicit'); + ('c', 'urn:ietf:params:oauth:grant_type:multiparty-delegation'); -- -- Merge the temporary clients safely into the database. This is a two-step process to keep clients from being created on every startup with a persistent store. diff --git a/uma-server-webapp/src/main/webapp/WEB-INF/authz-config.xml b/uma-server-webapp/src/main/webapp/WEB-INF/authz-config.xml new file mode 100644 index 000000000..261f47f13 --- /dev/null +++ b/uma-server-webapp/src/main/webapp/WEB-INF/authz-config.xml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/uma-server/src/main/java/org/mitre/uma/exception/InvalidTicketException.java b/uma-server/src/main/java/org/mitre/uma/exception/InvalidTicketException.java new file mode 100644 index 000000000..9b0ffcc19 --- /dev/null +++ b/uma-server/src/main/java/org/mitre/uma/exception/InvalidTicketException.java @@ -0,0 +1,26 @@ +/******************************************************************************* + * Copyright 2015 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.uma.exception; + +/** + * @author jricher + * + */ +public class InvalidTicketException extends RuntimeException { + +} diff --git a/uma-server/src/main/java/org/mitre/uma/exception/NeedInfoException.java b/uma-server/src/main/java/org/mitre/uma/exception/NeedInfoException.java new file mode 100644 index 000000000..d40da4af0 --- /dev/null +++ b/uma-server/src/main/java/org/mitre/uma/exception/NeedInfoException.java @@ -0,0 +1,72 @@ +/******************************************************************************* + * Copyright 2015 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.uma.exception; + +import java.util.Collection; + +import org.mitre.uma.model.Claim; + +/** + * @author jricher + * + */ +public class NeedInfoException extends RuntimeException { + + private static final long serialVersionUID = -8886957523367481451L; + + private String ticketValue; + private Collection unmatched; + + /** + * @param ticketValue + * @param unmatched + */ + public NeedInfoException(String ticketValue, Collection unmatched) { + this.setTicketValue(ticketValue); + this.setUnmatched(unmatched); + } + + /** + * @return the ticketValue + */ + public String getTicketValue() { + return ticketValue; + } + + /** + * @param ticketValue the ticketValue to set + */ + public void setTicketValue(String ticketValue) { + this.ticketValue = ticketValue; + } + + /** + * @return the unmatched + */ + public Collection getUnmatched() { + return unmatched; + } + + /** + * @param unmatched the unmatched to set + */ + public void setUnmatched(Collection unmatched) { + this.unmatched = unmatched; + } + +} diff --git a/uma-server/src/main/java/org/mitre/uma/exception/NotAuthorizedException.java b/uma-server/src/main/java/org/mitre/uma/exception/NotAuthorizedException.java new file mode 100644 index 000000000..85f23e53b --- /dev/null +++ b/uma-server/src/main/java/org/mitre/uma/exception/NotAuthorizedException.java @@ -0,0 +1,26 @@ +/******************************************************************************* + * Copyright 2015 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.uma.exception; + +/** + * @author jricher + * + */ +public class NotAuthorizedException extends RuntimeException { + +} diff --git a/uma-server/src/main/java/org/mitre/uma/token/ExtendedWebResponseExceptionTranslator.java b/uma-server/src/main/java/org/mitre/uma/token/ExtendedWebResponseExceptionTranslator.java new file mode 100644 index 000000000..0803acb56 --- /dev/null +++ b/uma-server/src/main/java/org/mitre/uma/token/ExtendedWebResponseExceptionTranslator.java @@ -0,0 +1,30 @@ +/******************************************************************************* + * Copyright 2015 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.uma.token; + +import org.springframework.security.oauth2.provider.error.DefaultWebResponseExceptionTranslator; + +/** + * Class to handly the special multiparty error conditions, like "get more info". + * + * @author jricher + * + */ +public class ExtendedWebResponseExceptionTranslator extends DefaultWebResponseExceptionTranslator { + +} diff --git a/uma-server/src/main/java/org/mitre/uma/token/RequestingPartyTokenGranter.java b/uma-server/src/main/java/org/mitre/uma/token/RequestingPartyTokenGranter.java new file mode 100644 index 000000000..12e9b1d99 --- /dev/null +++ b/uma-server/src/main/java/org/mitre/uma/token/RequestingPartyTokenGranter.java @@ -0,0 +1,286 @@ +/******************************************************************************* + * Copyright 2015 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.uma.token; + +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.UUID; + +import org.mitre.jwt.signer.service.JWTSigningAndValidationService; +import org.mitre.oauth2.model.AuthenticationHolderEntity; +import org.mitre.oauth2.model.ClientDetailsEntity; +import org.mitre.oauth2.model.OAuth2AccessTokenEntity; +import org.mitre.oauth2.service.ClientDetailsEntityService; +import org.mitre.oauth2.service.OAuth2TokenEntityService; +import org.mitre.openid.connect.config.ConfigurationPropertiesBean; +import org.mitre.openid.connect.service.OIDCTokenService; +import org.mitre.openid.connect.view.HttpCodeView; +import org.mitre.openid.connect.view.JsonEntityView; +import org.mitre.openid.connect.view.JsonErrorView; +import org.mitre.uma.exception.InvalidTicketException; +import org.mitre.uma.exception.NeedInfoException; +import org.mitre.uma.exception.NotAuthorizedException; +import org.mitre.uma.model.Claim; +import org.mitre.uma.model.ClaimProcessingResult; +import org.mitre.uma.model.Permission; +import org.mitre.uma.model.PermissionTicket; +import org.mitre.uma.model.ResourceSet; +import org.mitre.uma.service.ClaimsProcessingService; +import org.mitre.uma.service.PermissionService; +import org.mitre.uma.service.UmaTokenService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.security.oauth2.common.OAuth2AccessToken; +import org.springframework.security.oauth2.provider.ClientDetails; +import org.springframework.security.oauth2.provider.ClientDetailsService; +import org.springframework.security.oauth2.provider.OAuth2Authentication; +import org.springframework.security.oauth2.provider.OAuth2Request; +import org.springframework.security.oauth2.provider.OAuth2RequestFactory; +import org.springframework.security.oauth2.provider.TokenRequest; +import org.springframework.security.oauth2.provider.token.AbstractTokenGranter; +import org.springframework.security.oauth2.provider.token.AuthorizationServerTokenServices; +import org.springframework.stereotype.Component; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.servlet.ModelAndView; + +import com.google.common.base.Strings; +import com.google.common.collect.Lists; +import com.google.common.collect.Sets; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; +import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jose.JWSHeader; +import com.nimbusds.jwt.JWTClaimsSet; +import com.nimbusds.jwt.SignedJWT; + +/** + * @author jricher + * + */ +@Component("requestingPartyTokenGranter") +public class RequestingPartyTokenGranter extends AbstractTokenGranter { + + private static final String grantType = "urn:ietf:params:oauth:grant_type:multiparty-delegation"; + + @Autowired + private PermissionService permissionService; + + @Autowired + private OAuth2TokenEntityService tokenService; + + @Autowired + private OIDCTokenService oidcTokenService; + + @Autowired + private ClaimsProcessingService claimsProcessingService; + + @Autowired + private UmaTokenService umaTokenService; + + @Autowired + private ClientDetailsEntityService clientService; + + @Autowired + private ConfigurationPropertiesBean config; + + @Autowired + private JWTSigningAndValidationService jwtService; + + + /** + * @param tokenServices + * @param clientDetailsService + * @param requestFactory + * @param grantType + */ + @Autowired + protected RequestingPartyTokenGranter(AuthorizationServerTokenServices tokenServices, ClientDetailsService clientDetailsService, OAuth2RequestFactory requestFactory) { + super(tokenServices, clientDetailsService, requestFactory, grantType); + } + + /* (non-Javadoc) + * @see org.springframework.security.oauth2.provider.token.AbstractTokenGranter#getOAuth2Authentication(org.springframework.security.oauth2.provider.ClientDetails, org.springframework.security.oauth2.provider.TokenRequest) + */ + @Override + protected OAuth2AccessToken getAccessToken(ClientDetails client, TokenRequest tokenRequest) { + + //String incomingTokenValue = tokenRequest.getRequestParameters().get("token"); + + String ticketValue = tokenRequest.getRequestParameters().get("ticket"); + + PermissionTicket ticket = permissionService.getByTicket(ticketValue); + OAuth2AccessTokenEntity incomingRpt = null; + String rptValue = tokenRequest.getRequestParameters().get("rpt"); + if (!Strings.isNullOrEmpty(rptValue)) { + incomingRpt = tokenService.readAccessToken(rptValue); + } + + if (ticket != null) { + // found the ticket, see if it's any good + + ResourceSet rs = ticket.getPermission().getResourceSet(); + + if (rs.getPolicies() == null || rs.getPolicies().isEmpty()) { + // the required claims are empty, this resource has no way to be authorized + + throw new NotAuthorizedException(); + } else { + // claims weren't empty or missing, we need to check against what we have + + ClaimProcessingResult result = claimsProcessingService.claimsAreSatisfied(rs, ticket); + + + if (result.isSatisfied()) { + // the service found what it was looking for, issue a token + + OAuth2AccessTokenEntity token = new OAuth2AccessTokenEntity(); + + OAuth2Request clientAuth = tokenRequest.createOAuth2Request(client); + OAuth2Authentication o2auth = new OAuth2Authentication(clientAuth, null); + AuthenticationHolderEntity authHolder = new AuthenticationHolderEntity(); + authHolder.setAuthentication(o2auth); + + token.setAuthenticationHolder(authHolder); + + ClientDetailsEntity clientEntity = clientService.loadClientByClientId(client.getClientId()); + token.setClient(clientEntity); + + Set ticketScopes = ticket.getPermission().getScopes(); + Set policyScopes = result.getMatched().getScopes(); + + Permission perm = new Permission(); + perm.setResourceSet(ticket.getPermission().getResourceSet()); + perm.setScopes(new HashSet<>(Sets.intersection(ticketScopes, policyScopes))); + + token.setPermissions(Sets.newHashSet(perm)); + + JWTClaimsSet.Builder claims = new JWTClaimsSet.Builder(); + + claims.audience(Lists.newArrayList(ticket.getPermission().getResourceSet().getId().toString())); + claims.issuer(config.getIssuer()); + claims.jwtID(UUID.randomUUID().toString()); + + if (config.getRqpTokenLifeTime() != null) { + Date exp = new Date(System.currentTimeMillis() + config.getRqpTokenLifeTime() * 1000L); + + claims.expirationTime(exp); + token.setExpiration(exp); + } + + + JWSAlgorithm signingAlgorithm = jwtService.getDefaultSigningAlgorithm(); + JWSHeader header = new JWSHeader(signingAlgorithm, null, null, null, null, null, null, null, null, null, + jwtService.getDefaultSignerKeyId(), + null, null); + SignedJWT signed = new SignedJWT(header, claims.build()); + + jwtService.signJwt(signed); + + token.setJwt(signed); + + tokenService.saveAccessToken(token); + + + // if we have an inbound RPT, throw it out because we're replacing it + if (incomingRpt != null) { + tokenService.revokeAccessToken(incomingRpt); + } + + return token; + } else { + + throw new NeedInfoException(ticketValue, result.getUnmatched()); + + } + + + } + } else { + throw new InvalidTicketException(); + } + + + + + } + + + @ExceptionHandler(NeedInfoException.class) + public ModelAndView handleUmaException(Exception e) { + // if we got here, the claim didn't match, forward the user to the claim gathering endpoint + + NeedInfoException nie = (NeedInfoException)e; + + JsonObject entity = new JsonObject(); + + entity.addProperty(JsonErrorView.ERROR, "need_info"); + JsonObject details = new JsonObject(); + + JsonObject rpClaims = new JsonObject(); + rpClaims.addProperty("redirect_user", true); + rpClaims.addProperty("ticket", nie.getTicketValue()); + JsonArray req = new JsonArray(); + for (Claim claim : nie.getUnmatched()) { + JsonObject c = new JsonObject(); + c.addProperty("name", claim.getName()); + c.addProperty("friendly_name", claim.getFriendlyName()); + c.addProperty("claim_type", claim.getClaimType()); + JsonArray f = new JsonArray(); + for (String format : claim.getClaimTokenFormat()) { + f.add(new JsonPrimitive(format)); + } + c.add("claim_token_format", f); + JsonArray i = new JsonArray(); + for (String issuer : claim.getIssuer()) { + i.add(new JsonPrimitive(issuer)); + } + c.add("issuer", i); + req.add(c); + } + rpClaims.add("required_claims", req); + details.add("requesting_party_claims", rpClaims); + entity.add("error_details", details); + + Map m = new HashMap<>(); + m.put(JsonEntityView.ENTITY, entity); + return new ModelAndView(JsonEntityView.VIEWNAME, m); + } + + @ExceptionHandler(InvalidTicketException.class) + public ModelAndView handleInvalidTicketException(Exception e) { + // ticket wasn't found, return an error + Map m = new HashMap<>(); + m.put(HttpCodeView.CODE, HttpStatus.BAD_REQUEST); + m.put(JsonErrorView.ERROR, "invalid_ticket"); + return new ModelAndView(JsonErrorView.VIEWNAME, m); + } + + @ExceptionHandler(NotAuthorizedException.class) + public ModelAndView handleNotAuthorizedException(Exception e) { + Map m = new HashMap<>(); + m.put(JsonErrorView.ERROR, "not_authorized"); + m.put(JsonErrorView.ERROR_MESSAGE, "This resource set can not be accessed."); + m.put(HttpCodeView.CODE, HttpStatus.FORBIDDEN); + return new ModelAndView(JsonErrorView.VIEWNAME, m); + } + +}