From 20f74269e11029273260edf39f6132f1f095cddb Mon Sep 17 00:00:00 2001 From: Justin Richer Date: Wed, 4 Jan 2012 16:39:26 -0500 Subject: [PATCH 1/4] updated some entity annotations --- .../mitre/openid/connect/model/Address.java | 22 ++++ .../openid/connect/model/ApprovedSite.java | 6 +- .../org/mitre/openid/connect/model/Event.java | 46 +++++++ .../mitre/openid/connect/model/IdToken.java | 124 +++++------------- .../mitre/openid/connect/model/UserInfo.java | 2 + .../openid/connect/model/WhitelistedSite.java | 4 +- 6 files changed, 113 insertions(+), 91 deletions(-) diff --git a/server/src/main/java/org/mitre/openid/connect/model/Address.java b/server/src/main/java/org/mitre/openid/connect/model/Address.java index f74b968e6..9fefc09b8 100644 --- a/server/src/main/java/org/mitre/openid/connect/model/Address.java +++ b/server/src/main/java/org/mitre/openid/connect/model/Address.java @@ -1,10 +1,14 @@ package org.mitre.openid.connect.model; import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; @Entity public class Address { + private Long id; private String formatted; private String street_address; private String locality; @@ -12,6 +16,8 @@ public class Address { private String postal_code; private String country; + + /** * @return the formatted */ @@ -84,5 +90,21 @@ public class Address { public void setCountry(String country) { this.country = country; } + + /** + * @return the id + */ + @Id + @GeneratedValue(strategy=GenerationType.IDENTITY) + public Long getId() { + return id; + } + + /** + * @param id the id to set + */ + public void setId(Long id) { + this.id = id; + } } diff --git a/server/src/main/java/org/mitre/openid/connect/model/ApprovedSite.java b/server/src/main/java/org/mitre/openid/connect/model/ApprovedSite.java index 3f16c4d03..4b6e4682a 100644 --- a/server/src/main/java/org/mitre/openid/connect/model/ApprovedSite.java +++ b/server/src/main/java/org/mitre/openid/connect/model/ApprovedSite.java @@ -8,6 +8,8 @@ import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; +import javax.persistence.ManyToOne; +import javax.persistence.OneToMany; import javax.persistence.Temporal; import org.springframework.security.oauth2.provider.ClientDetails; @@ -48,7 +50,7 @@ public class ApprovedSite { * @return the id */ @Id - @GeneratedValue(strategy = GenerationType.AUTO) + @GeneratedValue(strategy = GenerationType.IDENTITY) public Long getId() { return id; } @@ -63,6 +65,7 @@ public class ApprovedSite { /** * @return the userInfo */ + @ManyToOne public UserInfo getUserInfo() { return userInfo; } @@ -123,6 +126,7 @@ public class ApprovedSite { /** * @return the allowedScopes */ + @OneToMany public Collection getAllowedScopes() { return allowedScopes; } diff --git a/server/src/main/java/org/mitre/openid/connect/model/Event.java b/server/src/main/java/org/mitre/openid/connect/model/Event.java index d287ad996..bc776c423 100644 --- a/server/src/main/java/org/mitre/openid/connect/model/Event.java +++ b/server/src/main/java/org/mitre/openid/connect/model/Event.java @@ -2,7 +2,12 @@ package org.mitre.openid.connect.model; import java.util.Date; +import javax.persistence.Basic; import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Temporal; /** * Class to contain a logged event in the system. @@ -19,5 +24,46 @@ public class Event { private Long id; private EventType type; private Date timestamp; + + /** + * @return the id + */ + @Id + @GeneratedValue(strategy=GenerationType.IDENTITY) + public Long getId() { + return id; + } + /** + * @param id the id to set + */ + public void setId(Long id) { + this.id = id; + } + /** + * @return the type + */ + public EventType getType() { + return type; + } + /** + * @param type the type to set + */ + public void setType(EventType type) { + this.type = type; + } + /** + * @return the timestamp + */ + @Basic + @Temporal(javax.persistence.TemporalType.TIMESTAMP) + public Date getTimestamp() { + return timestamp; + } + /** + * @param timestamp the timestamp to set + */ + public void setTimestamp(Date timestamp) { + this.timestamp = timestamp; + } } diff --git a/server/src/main/java/org/mitre/openid/connect/model/IdToken.java b/server/src/main/java/org/mitre/openid/connect/model/IdToken.java index 5a08edb4f..a7df4a097 100644 --- a/server/src/main/java/org/mitre/openid/connect/model/IdToken.java +++ b/server/src/main/java/org/mitre/openid/connect/model/IdToken.java @@ -1,105 +1,51 @@ package org.mitre.openid.connect.model; import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; import org.mitre.jwt.model.Jwt; +import org.mitre.jwt.model.JwtClaims; /* * TODO: This class needs to be encoded as a JWT */ @Entity -public class IdToken extends Jwt { +public class IdToken extends JwtClaims { - private String iss; - private String user_id; - private String aud; - private String exp; - private String iso29115; - private String nonce; - private String auth_time; + public static final String USER_ID = "user_id"; + public static final String ISO29115 = "iso29115"; + public static final String NONCE = "nonce"; + public static final String AUTH_TIME = "auth_time"; + + private Long id; + /** - * @return the iss - */ - public String getIss() { - return iss; - } + * @return the id + */ + @Id + @GeneratedValue(strategy=GenerationType.IDENTITY) + public Long getId() { + return id; + } /** - * @param iss the iss to set - */ - public void setIss(String iss) { - this.iss = iss; - } - /** - * @return the user_id - */ - public String getUser_id() { - return user_id; - } - /** - * @param user_id the user_id to set - */ - public void setUser_id(String user_id) { - this.user_id = user_id; - } - /** - * @return the aud - */ - public String getAud() { - return aud; - } - /** - * @param aud the aud to set - */ - public void setAud(String aud) { - this.aud = aud; - } - /** - * @return the exp - */ - public String getExp() { - return exp; - } - /** - * @param exp the exp to set - */ - public void setExp(String exp) { - this.exp = exp; - } - /** - * @return the iso29115 - */ - public String getIso29115() { - return iso29115; - } - /** - * @param iso29115 the iso29115 to set - */ - public void setIso29115(String iso29115) { - this.iso29115 = iso29115; - } - /** - * @return the nonce - */ - public String getNonce() { - return nonce; - } - /** - * @param nonce the nonce to set - */ - public void setNonce(String nonce) { - this.nonce = nonce; - } - /** - * @return the auth_time - */ - public String getAuth_time() { - return auth_time; - } - /** - * @param auth_time the auth_time to set - */ - public void setAuth_time(String auth_time) { - this.auth_time = auth_time; + * @param id the id to set + */ + public void setId(Long id) { + this.id = id; + } + + + public String getUserId() { + return getClaimAsString(USER_ID); } + public void setUserId(String user_id) { + setClaim(USER_ID, user_id); + } + + + // TODO: add in other fields + } diff --git a/server/src/main/java/org/mitre/openid/connect/model/UserInfo.java b/server/src/main/java/org/mitre/openid/connect/model/UserInfo.java index 67c8a2687..acbebd898 100644 --- a/server/src/main/java/org/mitre/openid/connect/model/UserInfo.java +++ b/server/src/main/java/org/mitre/openid/connect/model/UserInfo.java @@ -1,6 +1,7 @@ package org.mitre.openid.connect.model; import javax.persistence.Entity; +import javax.persistence.Id; @Entity public class UserInfo { @@ -29,6 +30,7 @@ public class UserInfo { /** * @return the user_id */ + @Id public String getUser_id() { return user_id; } diff --git a/server/src/main/java/org/mitre/openid/connect/model/WhitelistedSite.java b/server/src/main/java/org/mitre/openid/connect/model/WhitelistedSite.java index ca5b4987d..019988888 100644 --- a/server/src/main/java/org/mitre/openid/connect/model/WhitelistedSite.java +++ b/server/src/main/java/org/mitre/openid/connect/model/WhitelistedSite.java @@ -6,6 +6,7 @@ import java.util.Date; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; +import javax.persistence.ManyToOne; import org.springframework.security.oauth2.provider.ClientDetails; @@ -19,7 +20,7 @@ public class WhitelistedSite { // unique id @Id - @GeneratedValue(strategy = GenerationType.AUTO) + @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; // who added this site to the whitelist (should be an admin) @@ -30,5 +31,6 @@ public class WhitelistedSite { // what scopes be allowed by default // this should include all information for what data to access + @ManyToOne private Collection allowedScopes; } From c63eb440081a6d2ddb73625a997ad000f6511e1b Mon Sep 17 00:00:00 2001 From: Justin Richer Date: Fri, 6 Jan 2012 16:27:11 -0500 Subject: [PATCH 2/4] imported oauth2 support code from PuSHEE --- .../exception/ClientNotFoundException.java | 50 ++ .../exception/DuplicateClientIdException.java | 15 + .../exception/PermissionDeniedException.java | 49 ++ .../oauth2/model/ClientDetailsEntity.java | 473 ++++++++++++++++++ .../model/ClientDetailsEntityFactory.java | 9 + .../oauth2/model/ClientGeneratorFactory.java | 32 ++ .../oauth2/model/OAuth2AccessTokenEntity.java | 210 ++++++++ .../model/OAuth2AccessTokenEntityFactory.java | 7 + .../model/OAuth2RefreshTokenEntity.java | 136 +++++ .../OAuth2RefreshTokenEntityFactory.java | 7 + .../mitre/oauth2/model/UUIDTokenFactory.java | 39 ++ .../model/serializer/JSONOAuthClientView.java | 66 +++ .../model/serializer/TokenIntrospection.java | 100 ++++ .../repository/OAuth2ClientRepository.java | 22 + .../repository/OAuth2TokenRepository.java | 35 ++ .../impl/JpaOAuth2ClientRepository.java | 74 +++ .../impl/JpaOAuth2TokenRepository.java | 138 +++++ .../service/ClientDetailsEntityService.java | 22 + .../service/OAuth2TokenEntityService.java | 26 + ...faultOAuth2ClientDetailsEntityService.java | 130 +++++ .../DefaultOAuth2ProviderTokenService.java | 319 ++++++++++++ .../oauth2/web/IntrospectionEndpoint.java | 43 ++ .../org/mitre/oauth2/web/OAuthClientAPI.java | 201 ++++++++ .../oauth2/web/OAuthClientController.java | 166 ++++++ .../web/OAuthConfirmationController.java | 70 +++ .../mitre/oauth2/web/RevocationEndpoint.java | 79 +++ .../{IdToken.java => IdTokenClaims.java} | 32 +- .../connect/repository/IdTokenRepository.java | 6 +- .../openid/connect/web/CheckIDEndpoint.java | 4 +- .../impl/OpenIDUserDetailsService.java | 98 ++++ .../main/java/org/mitre/util/jpa/JpaUtil.java | 37 ++ spring-security-oauth | 2 +- upstream-patch/secoauth_183.patch | 113 +++++ 33 files changed, 2801 insertions(+), 9 deletions(-) create mode 100644 server/src/main/java/org/mitre/oauth2/exception/ClientNotFoundException.java create mode 100644 server/src/main/java/org/mitre/oauth2/exception/DuplicateClientIdException.java create mode 100644 server/src/main/java/org/mitre/oauth2/exception/PermissionDeniedException.java create mode 100644 server/src/main/java/org/mitre/oauth2/model/ClientDetailsEntity.java create mode 100644 server/src/main/java/org/mitre/oauth2/model/ClientDetailsEntityFactory.java create mode 100644 server/src/main/java/org/mitre/oauth2/model/ClientGeneratorFactory.java create mode 100644 server/src/main/java/org/mitre/oauth2/model/OAuth2AccessTokenEntity.java create mode 100644 server/src/main/java/org/mitre/oauth2/model/OAuth2AccessTokenEntityFactory.java create mode 100644 server/src/main/java/org/mitre/oauth2/model/OAuth2RefreshTokenEntity.java create mode 100644 server/src/main/java/org/mitre/oauth2/model/OAuth2RefreshTokenEntityFactory.java create mode 100644 server/src/main/java/org/mitre/oauth2/model/UUIDTokenFactory.java create mode 100644 server/src/main/java/org/mitre/oauth2/model/serializer/JSONOAuthClientView.java create mode 100644 server/src/main/java/org/mitre/oauth2/model/serializer/TokenIntrospection.java create mode 100644 server/src/main/java/org/mitre/oauth2/repository/OAuth2ClientRepository.java create mode 100644 server/src/main/java/org/mitre/oauth2/repository/OAuth2TokenRepository.java create mode 100644 server/src/main/java/org/mitre/oauth2/repository/impl/JpaOAuth2ClientRepository.java create mode 100644 server/src/main/java/org/mitre/oauth2/repository/impl/JpaOAuth2TokenRepository.java create mode 100644 server/src/main/java/org/mitre/oauth2/service/ClientDetailsEntityService.java create mode 100644 server/src/main/java/org/mitre/oauth2/service/OAuth2TokenEntityService.java create mode 100644 server/src/main/java/org/mitre/oauth2/service/impl/DefaultOAuth2ClientDetailsEntityService.java create mode 100644 server/src/main/java/org/mitre/oauth2/service/impl/DefaultOAuth2ProviderTokenService.java create mode 100644 server/src/main/java/org/mitre/oauth2/web/IntrospectionEndpoint.java create mode 100644 server/src/main/java/org/mitre/oauth2/web/OAuthClientAPI.java create mode 100644 server/src/main/java/org/mitre/oauth2/web/OAuthClientController.java create mode 100644 server/src/main/java/org/mitre/oauth2/web/OAuthConfirmationController.java create mode 100644 server/src/main/java/org/mitre/oauth2/web/RevocationEndpoint.java rename server/src/main/java/org/mitre/openid/connect/model/{IdToken.java => IdTokenClaims.java} (54%) create mode 100644 server/src/main/java/org/mitre/pushee/openid/service/impl/OpenIDUserDetailsService.java create mode 100644 server/src/main/java/org/mitre/util/jpa/JpaUtil.java create mode 100644 upstream-patch/secoauth_183.patch diff --git a/server/src/main/java/org/mitre/oauth2/exception/ClientNotFoundException.java b/server/src/main/java/org/mitre/oauth2/exception/ClientNotFoundException.java new file mode 100644 index 000000000..94fe88eff --- /dev/null +++ b/server/src/main/java/org/mitre/oauth2/exception/ClientNotFoundException.java @@ -0,0 +1,50 @@ +package org.mitre.oauth2.exception; +/** + * + */ + + +/** + * @author aanganes + * + */ +public class ClientNotFoundException extends RuntimeException { + + /** + * + */ + private static final Long serialVersionUID = 1L; + + /** + * + */ + public ClientNotFoundException() { + // TODO Auto-generated constructor stub + } + + /** + * @param message + */ + public ClientNotFoundException(String message) { + super(message); + // TODO Auto-generated constructor stub + } + + /** + * @param cause + */ + public ClientNotFoundException(Throwable cause) { + super(cause); + // TODO Auto-generated constructor stub + } + + /** + * @param message + * @param cause + */ + public ClientNotFoundException(String message, Throwable cause) { + super(message, cause); + // TODO Auto-generated constructor stub + } + +} diff --git a/server/src/main/java/org/mitre/oauth2/exception/DuplicateClientIdException.java b/server/src/main/java/org/mitre/oauth2/exception/DuplicateClientIdException.java new file mode 100644 index 000000000..78ff82b2c --- /dev/null +++ b/server/src/main/java/org/mitre/oauth2/exception/DuplicateClientIdException.java @@ -0,0 +1,15 @@ +package org.mitre.oauth2.exception; + +public class DuplicateClientIdException extends RuntimeException { + + public DuplicateClientIdException(String clientId) { + super("Duplicate client id: " + clientId); + } + + /** + * + */ + private static final long serialVersionUID = 1L; + + +} diff --git a/server/src/main/java/org/mitre/oauth2/exception/PermissionDeniedException.java b/server/src/main/java/org/mitre/oauth2/exception/PermissionDeniedException.java new file mode 100644 index 000000000..c6f2bea6e --- /dev/null +++ b/server/src/main/java/org/mitre/oauth2/exception/PermissionDeniedException.java @@ -0,0 +1,49 @@ +/** + * + */ +package org.mitre.oauth2.exception; + +/** + * @author AANGANES + * + */ +public class PermissionDeniedException extends RuntimeException { + + /** + * + */ + private static final Long serialVersionUID = 1L; + + /** + * + */ + public PermissionDeniedException() { + // TODO Auto-generated constructor stub + } + + /** + * @param message + */ + public PermissionDeniedException(String message) { + super(message); + // TODO Auto-generated constructor stub + } + + /** + * @param cause + */ + public PermissionDeniedException(Throwable cause) { + super(cause); + // TODO Auto-generated constructor stub + } + + /** + * @param message + * @param cause + */ + public PermissionDeniedException(String message, Throwable cause) { + super(message, cause); + // TODO Auto-generated constructor stub + } + +} diff --git a/server/src/main/java/org/mitre/oauth2/model/ClientDetailsEntity.java b/server/src/main/java/org/mitre/oauth2/model/ClientDetailsEntity.java new file mode 100644 index 000000000..247515d7b --- /dev/null +++ b/server/src/main/java/org/mitre/oauth2/model/ClientDetailsEntity.java @@ -0,0 +1,473 @@ +/** + * + */ +package org.mitre.oauth2.model; + +import java.util.Collections; +import java.util.List; +import java.util.Set; + +import javax.persistence.Basic; +import javax.persistence.CollectionTable; +import javax.persistence.ElementCollection; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.NamedQueries; +import javax.persistence.NamedQuery; +import javax.persistence.Table; + +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.oauth2.provider.ClientDetails; + +/** + * @author jricher + * + */ +@Entity +@Table(name="clientdetails") +@NamedQueries({ + @NamedQuery(name = "ClientDetailsEntity.findAll", query = "SELECT c FROM ClientDetailsEntity c") +}) +public class ClientDetailsEntity implements ClientDetails { + + /** + * Create a blank ClientDetailsEntity + */ + public ClientDetailsEntity() { + + } + + private String clientId; + private String clientSecret; + private Set scope; + private Set authorizedGrantTypes; + private Set authorities = Collections.emptySet(); + private String clientName; + private String clientDescription; + private boolean allowRefresh = false; // do we allow refresh tokens for this client? + private Long accessTokenTimeout; // in seconds + private Long refreshTokenTimeout; // in seconds + private String owner; // userid of who registered it + private String registeredRedirectUri; + private Set resourceIds; + + // TODO: + /* + private boolean allowMultipleAccessTokens; // do we allow multiple access tokens, or not? + private boolean reuseRefreshToken; // do we let someone reuse a refresh token? + */ + + /** + * @return the clientId + */ + @Id + public String getClientId() { + return clientId; + } + + /** + * @param clientId The OAuth2 client_id, must be unique to this client + */ + public void setClientId(String clientId) { + this.clientId = clientId; + } + + /** + * @return the clientSecret + */ + @Basic + public String getClientSecret() { + return clientSecret; + } + + /** + * @param clientSecret the OAuth2 client_secret (optional) + */ + public void setClientSecret(String clientSecret) { + this.clientSecret = clientSecret; + } + + /** + * @return the scope + */ + @ElementCollection(fetch = FetchType.EAGER) + @CollectionTable( + name="scope", + joinColumns=@JoinColumn(name="owner_id") + ) + public Set getScope() { + return scope; + } + + /** + * @param scope the set of scopes allowed to be issued to this client + */ + public void setScope(Set scope) { + this.scope = scope; + } + + /** + * @return the authorizedGrantTypes + */ + @ElementCollection(fetch = FetchType.EAGER) + @CollectionTable( + name="authorizedgranttypes", + joinColumns=@JoinColumn(name="owner_id") + ) + public Set getAuthorizedGrantTypes() { + return authorizedGrantTypes; + } + + /** + * @param authorizedGrantTypes the OAuth2 grant types that this client is allowed to use + */ + public void setAuthorizedGrantTypes(Set authorizedGrantTypes) { + this.authorizedGrantTypes = authorizedGrantTypes; + } + + /** + * @return the authorities + */ + @ElementCollection(fetch = FetchType.EAGER) + @CollectionTable( + name="authorities", + joinColumns=@JoinColumn(name="owner_id") + ) + public Set getAuthorities() { + return authorities; + } + + /** + * @param authorities the Spring Security authorities this client is given + */ + public void setAuthorities(Set authorities) { + this.authorities = authorities; + } + + /** + * If the clientSecret is not null, then it is always required. + */ + @Override + public boolean isSecretRequired() { + return getClientSecret() != null; + } + + /** + * If the scope list is not null or empty, then this client has been scoped. + */ + @Override + public boolean isScoped() { + return getScope() != null && !getScope().isEmpty(); + } + + /** + * @return the clientName + */ + @Basic + public String getClientName() { + return clientName; + } + + /** + * @param clientName Human-readable name of the client (optional) + */ + public void setClientName(String clientName) { + this.clientName = clientName; + } + + /** + * @return the clientDescription + */ + @Basic + public String getClientDescription() { + return clientDescription; + } + + /** + * @param clientDescription Human-readable long description of the client (optional) + */ + public void setClientDescription(String clientDescription) { + this.clientDescription = clientDescription; + } + + /** + * @return the allowRefresh + */ + @Basic + public boolean isAllowRefresh() { + return allowRefresh; + } + + /** + * @param allowRefresh Whether to allow for issuance of refresh tokens or not (defaults to false) + */ + public void setAllowRefresh(boolean allowRefresh) { + this.allowRefresh = allowRefresh; + } + + /** + * @param accessTokenTimeout Lifetime of access tokens, in seconds (optional - leave null for no timeout) + */ + @Basic + public Long getAccessTokenTimeout() { + return accessTokenTimeout; + } + + /** + * @param accessTokenTimeout the accessTokenTimeout to set + */ + public void setAccessTokenTimeout(Long accessTokenTimeout) { + this.accessTokenTimeout = accessTokenTimeout; + } + + /** + * @return the refreshTokenTimeout + */ + @Basic + public Long getRefreshTokenTimeout() { + return refreshTokenTimeout; + } + + /** + * @param refreshTokenTimeout Lifetime of refresh tokens, in seconds (optional - leave null for no timeout) + */ + public void setRefreshTokenTimeout(Long refreshTokenTimeout) { + this.refreshTokenTimeout = refreshTokenTimeout; + } + + /** + * @return the owner + */ + @Basic + public String getOwner() { + return owner; + } + + /** + * @param owner User ID of the person who registered this client (optional) + */ + public void setOwner(String owner) { + this.owner = owner; + } + + /** + * @return the registeredRedirectUri + */ + @Basic + public String getRegisteredRedirectUri() { + return registeredRedirectUri; + } + + /** + * @param registeredRedirectUri the registeredRedirectUri to set + */ + public void setRegisteredRedirectUri(String registeredRedirectUri) { + this.registeredRedirectUri = registeredRedirectUri; + } + + /** + * @return the resourceIds + */ + public Set getResourceIds() { + return resourceIds; + } + + /** + * @param resourceIds the resourceIds to set + */ + @ElementCollection(fetch = FetchType.EAGER) + @CollectionTable( + name="resource_ids", + joinColumns=@JoinColumn(name="owner_id") + ) + public void setResourceIds(Set resourceIds) { + this.resourceIds = resourceIds; + } + + /* (non-Javadoc) + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return "ClientDetailsEntity [" + (clientId != null ? "clientId=" + clientId + ", " : "") + (scope != null ? "scope=" + scope + ", " : "") + (clientName != null ? "clientName=" + clientName + ", " : "") + (owner != null ? "owner=" + owner : "") + "]"; + } + + /* (non-Javadoc) + * @see java.lang.Object#hashCode() + */ + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((clientId == null) ? 0 : clientId.hashCode()); + return result; + } + + /* (non-Javadoc) + * @see java.lang.Object#equals(java.lang.Object) + */ + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + ClientDetailsEntity other = (ClientDetailsEntity) obj; + if (clientId == null) { + if (other.clientId != null) { + return false; + } + } else if (!clientId.equals(other.clientId)) { + return false; + } + return true; + } + + public static ClientDetailsEntityBuilder makeBuilder() { + return new ClientDetailsEntityBuilder(); + } + + public static class ClientDetailsEntityBuilder { + private ClientDetailsEntity instance; + + private ClientDetailsEntityBuilder() { + instance = new ClientDetailsEntity(); + } + + /** + * @param clientId + * @see org.mitre.oauth2.model.ClientDetailsEntity#setClientId(java.lang.String) + */ + public ClientDetailsEntityBuilder setClientId(String clientId) { + instance.setClientId(clientId); + return this; + } + + /** + * @param clientSecret + * @see org.mitre.oauth2.model.ClientDetailsEntity#setClientSecret(java.lang.String) + */ + public ClientDetailsEntityBuilder setClientSecret(String clientSecret) { + instance.setClientSecret(clientSecret); + return this; + } + + /** + * @param scope + * @see org.mitre.oauth2.model.ClientDetailsEntity#setScope(java.util.List) + */ + public ClientDetailsEntityBuilder setScope(Set scope) { + instance.setScope(scope); + return this; + } + + /** + * @param authorizedGrantTypes + * @see org.mitre.oauth2.model.ClientDetailsEntity#setAuthorizedGrantTypes(java.util.List) + */ + public ClientDetailsEntityBuilder setAuthorizedGrantTypes(Set authorizedGrantTypes) { + instance.setAuthorizedGrantTypes(authorizedGrantTypes); + return this; + } + + /** + * @param authorities + * @see org.mitre.oauth2.model.ClientDetailsEntity#setAuthorities(java.util.List) + */ + public ClientDetailsEntityBuilder setAuthorities(Set authorities) { + instance.setAuthorities(authorities); + return this; + } + + /** + * @param clientName + * @see org.mitre.oauth2.model.ClientDetailsEntity#setClientName(java.lang.String) + */ + public ClientDetailsEntityBuilder setClientName(String clientName) { + instance.setClientName(clientName); + return this; + } + + /** + * @param clientDescription + * @see org.mitre.oauth2.model.ClientDetailsEntity#setClientDescription(java.lang.String) + */ + public ClientDetailsEntityBuilder setClientDescription(String clientDescription) { + instance.setClientDescription(clientDescription); + return this; + } + + /** + * @param allowRefresh + * @see org.mitre.oauth2.model.ClientDetailsEntity#setAllowRefresh(boolean) + */ + public ClientDetailsEntityBuilder setAllowRefresh(boolean allowRefresh) { + instance.setAllowRefresh(allowRefresh); + return this; + } + + /** + * @param accessTokenTimeout + * @see org.mitre.oauth2.model.ClientDetailsEntity#setAccessTokenTimeout(java.lang.Long) + */ + public ClientDetailsEntityBuilder setAccessTokenTimeout(Long accessTokenTimeout) { + instance.setAccessTokenTimeout(accessTokenTimeout); + return this; + } + + /** + * @param refreshTokenTimeout + * @see org.mitre.oauth2.model.ClientDetailsEntity#setRefreshTokenTimeout(java.lang.Long) + */ + public ClientDetailsEntityBuilder setRefreshTokenTimeout(Long refreshTokenTimeout) { + instance.setRefreshTokenTimeout(refreshTokenTimeout); + return this; + } + + /** + * @param owner + * @see org.mitre.oauth2.model.ClientDetailsEntity#setOwner(java.lang.String) + */ + public ClientDetailsEntityBuilder setOwner(String owner) { + instance.setOwner(owner); + return this; + } + + + + /** + * Complete the builder + * @return + */ + public ClientDetailsEntity finish() { + return instance; + } + + /** + * @param registeredRedirectUri + * @see org.mitre.oauth2.model.ClientDetailsEntity#setRegisteredRedirectUri(java.lang.String) + */ + public ClientDetailsEntityBuilder setRegisteredRedirectUri(String registeredRedirectUri) { + instance.setRegisteredRedirectUri(registeredRedirectUri); + return this; + } + + /** + * @param resourceIds + * @see org.mitre.oauth2.model.ClientDetailsEntity#setResourceIds(java.util.List) + */ + public ClientDetailsEntityBuilder setResourceIds(Set resourceIds) { + instance.setResourceIds(resourceIds); + return this; + } + + } + +} diff --git a/server/src/main/java/org/mitre/oauth2/model/ClientDetailsEntityFactory.java b/server/src/main/java/org/mitre/oauth2/model/ClientDetailsEntityFactory.java new file mode 100644 index 000000000..5f9f68ccb --- /dev/null +++ b/server/src/main/java/org/mitre/oauth2/model/ClientDetailsEntityFactory.java @@ -0,0 +1,9 @@ +package org.mitre.oauth2.model; + +import org.mitre.oauth2.model.ClientDetailsEntity.ClientDetailsEntityBuilder; + +public interface ClientDetailsEntityFactory { + + public ClientDetailsEntity createClient(String clientId, String clientSecret); + +} diff --git a/server/src/main/java/org/mitre/oauth2/model/ClientGeneratorFactory.java b/server/src/main/java/org/mitre/oauth2/model/ClientGeneratorFactory.java new file mode 100644 index 000000000..ceaa22cf6 --- /dev/null +++ b/server/src/main/java/org/mitre/oauth2/model/ClientGeneratorFactory.java @@ -0,0 +1,32 @@ +package org.mitre.oauth2.model; + +import java.util.UUID; + +import org.apache.commons.codec.binary.Base64; +import org.mitre.oauth2.model.ClientDetailsEntity.ClientDetailsEntityBuilder; +import org.springframework.stereotype.Service; + +/** + * A factory for making OAuth2 clients with autogenerated IDs and secrets (as desired) + * @author jricher + * + */ +@Service +public class ClientGeneratorFactory implements ClientDetailsEntityFactory { + + @Override + public ClientDetailsEntity createClient(String clientId, String clientSecret) { + ClientDetailsEntityBuilder builder = ClientDetailsEntity.makeBuilder(); + if (clientId == null) { + clientId = UUID.randomUUID().toString(); + } + builder.setClientId(clientId); + if (clientSecret == null) { + clientSecret = Base64.encodeBase64((UUID.randomUUID().toString() + UUID.randomUUID().toString()).getBytes()).toString(); + } + builder.setClientSecret(clientSecret); + + return builder.finish(); + } + +} diff --git a/server/src/main/java/org/mitre/oauth2/model/OAuth2AccessTokenEntity.java b/server/src/main/java/org/mitre/oauth2/model/OAuth2AccessTokenEntity.java new file mode 100644 index 000000000..11aebde73 --- /dev/null +++ b/server/src/main/java/org/mitre/oauth2/model/OAuth2AccessTokenEntity.java @@ -0,0 +1,210 @@ +/** + * + */ +package org.mitre.oauth2.model; + +import java.util.Date; +import java.util.Set; + +import javax.persistence.Basic; +import javax.persistence.CollectionTable; +import javax.persistence.Column; +import javax.persistence.ElementCollection; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.Lob; +import javax.persistence.ManyToOne; +import javax.persistence.NamedQueries; +import javax.persistence.NamedQuery; +import javax.persistence.OneToMany; +import javax.persistence.Table; +import javax.persistence.Temporal; +import javax.persistence.Transient; + +import org.springframework.security.oauth2.common.OAuth2AccessToken; +import org.springframework.security.oauth2.common.OAuth2RefreshToken; +import org.springframework.security.oauth2.provider.OAuth2Authentication; + +/** + * @author jricher + * + */ +@Entity +@Table(name="accesstoken") +@NamedQueries({ + @NamedQuery(name = "OAuth2AccessTokenEntity.getByRefreshToken", query = "select a from OAuth2AccessTokenEntity a where a.refreshToken = :refreshToken"), + @NamedQuery(name = "OAuth2AccessTokenEntity.getByClient", query = "select a from OAuth2AccessTokenEntity a where a.client = :client"), + @NamedQuery(name = "OAuth2AccessTokenEntity.getExpired", query = "select a from OAuth2AccessTokenEntity a where a.expiration is not null and a.expiration < current_timestamp") +}) +public class OAuth2AccessTokenEntity extends OAuth2AccessToken { + + private ClientDetailsEntity client; + + private OAuth2Authentication authentication; // the authentication that made this access + + /** + * + */ + public OAuth2AccessTokenEntity() { + super(null); + } + + + /** + * @return the authentication + */ + @Lob + @Basic + public OAuth2Authentication getAuthentication() { + return authentication; + } + + + /** + * @param authentication the authentication to set + */ + public void setAuthentication(OAuth2Authentication authentication) { + this.authentication = authentication; + } + + + /** + * @return the client + */ + @ManyToOne + @JoinColumn(name = "client_id") + public ClientDetailsEntity getClient() { + return client; + } + + + /** + * @param client the client to set + */ + public void setClient(ClientDetailsEntity client) { + this.client = client; + } + + /* (non-Javadoc) + * @see org.springframework.security.oauth2.common.OAuth2AccessToken#getValue() + */ + @Override + @Id + @Column(name="id") + public String getValue() { + // TODO Auto-generated method stub + return super.getValue(); + } + + /* (non-Javadoc) + * @see org.springframework.security.oauth2.common.OAuth2AccessToken#setValue(java.lang.String) + */ + @Override + public void setValue(String value) { + // TODO Auto-generated method stub + super.setValue(value); + } + + /* (non-Javadoc) + * @see org.springframework.security.oauth2.common.OAuth2AccessToken#getExpiration() + */ + @Override + @Basic + @Temporal(javax.persistence.TemporalType.TIMESTAMP) + public Date getExpiration() { + // TODO Auto-generated method stub + return super.getExpiration(); + } + + /* (non-Javadoc) + * @see org.springframework.security.oauth2.common.OAuth2AccessToken#setExpiration(java.util.Date) + */ + @Override + public void setExpiration(Date expiration) { + // TODO Auto-generated method stub + super.setExpiration(expiration); + } + + /* (non-Javadoc) + * @see org.springframework.security.oauth2.common.OAuth2AccessToken#getTokenType() + */ + @Override + @Basic + public String getTokenType() { + // TODO Auto-generated method stub + return super.getTokenType(); + } + + /* (non-Javadoc) + * @see org.springframework.security.oauth2.common.OAuth2AccessToken#setTokenType(java.lang.String) + */ + @Override + public void setTokenType(String tokenType) { + // TODO Auto-generated method stub + super.setTokenType(tokenType); + } + + /* (non-Javadoc) + * @see org.springframework.security.oauth2.common.OAuth2AccessToken#getRefreshToken() + */ + @Override + @ManyToOne + @JoinColumn(name="refresh_token_id") + public OAuth2RefreshTokenEntity getRefreshToken() { + // TODO Auto-generated method stub + return (OAuth2RefreshTokenEntity) super.getRefreshToken(); + } + + /* (non-Javadoc) + * @see org.springframework.security.oauth2.common.OAuth2AccessToken#setRefreshToken(org.springframework.security.oauth2.common.OAuth2RefreshToken) + */ + public void setRefreshToken(OAuth2RefreshTokenEntity refreshToken) { + // TODO Auto-generated method stub + super.setRefreshToken(refreshToken); + } + + /* (non-Javadoc) + * @see org.springframework.security.oauth2.common.OAuth2AccessToken#setRefreshToken(org.springframework.security.oauth2.common.OAuth2RefreshToken) + */ + @Override + public void setRefreshToken(OAuth2RefreshToken refreshToken) { + if (!(refreshToken instanceof OAuth2RefreshTokenEntity)) { + // TODO: make a copy constructor instead.... + throw new IllegalArgumentException("Not a storable refresh token entity!"); + } + // force a pass through to the entity version + setRefreshToken((OAuth2RefreshTokenEntity)refreshToken); + } + + /* (non-Javadoc) + * @see org.springframework.security.oauth2.common.OAuth2AccessToken#getScope() + */ + @Override + @ElementCollection(fetch=FetchType.EAGER) + @CollectionTable( + joinColumns=@JoinColumn(name="owner_id"), + name="scope" + ) + public Set getScope() { + // TODO Auto-generated method stub + return super.getScope(); + } + + /* (non-Javadoc) + * @see org.springframework.security.oauth2.common.OAuth2AccessToken#setScope(java.util.Set) + */ + @Override + public void setScope(Set scope) { + // TODO Auto-generated method stub + super.setScope(scope); + } + + @Transient + public boolean isExpired() { + return getExpiration() == null ? false : System.currentTimeMillis() > getExpiration().getTime(); + } + + +} diff --git a/server/src/main/java/org/mitre/oauth2/model/OAuth2AccessTokenEntityFactory.java b/server/src/main/java/org/mitre/oauth2/model/OAuth2AccessTokenEntityFactory.java new file mode 100644 index 000000000..b4bd1ba38 --- /dev/null +++ b/server/src/main/java/org/mitre/oauth2/model/OAuth2AccessTokenEntityFactory.java @@ -0,0 +1,7 @@ +package org.mitre.oauth2.model; + +public interface OAuth2AccessTokenEntityFactory { + + public OAuth2AccessTokenEntity createNewAccessToken(); + +} diff --git a/server/src/main/java/org/mitre/oauth2/model/OAuth2RefreshTokenEntity.java b/server/src/main/java/org/mitre/oauth2/model/OAuth2RefreshTokenEntity.java new file mode 100644 index 000000000..72d73f48f --- /dev/null +++ b/server/src/main/java/org/mitre/oauth2/model/OAuth2RefreshTokenEntity.java @@ -0,0 +1,136 @@ +/** + * + */ +package org.mitre.oauth2.model; + +import java.util.Date; +import java.util.Set; + +import javax.persistence.Basic; +import javax.persistence.CollectionTable; +import javax.persistence.Column; +import javax.persistence.ElementCollection; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.NamedQueries; +import javax.persistence.NamedQuery; +import javax.persistence.Table; +import javax.persistence.Temporal; +import javax.persistence.Transient; + +import org.springframework.security.oauth2.common.ExpiringOAuth2RefreshToken; + +/** + * @author jricher + * + */ +@Entity +@Table(name="refreshtoken") +@NamedQueries({ + @NamedQuery(name = "OAuth2RefreshTokenEntity.getByClient", query = "select r from OAuth2RefreshTokenEntity r where r.client = :client"), + @NamedQuery(name = "OAuth2RefreshTokenEntity.getExpired", query = "select r from OAuth2RefreshTokenEntity r where r.expiration is not null and r.expiration < current_timestamp") +}) +public class OAuth2RefreshTokenEntity extends ExpiringOAuth2RefreshToken { + + private ClientDetailsEntity client; + + private Set scope; // we save the scope issued to the refresh token so that we can reissue a new access token + + /** + * + */ + public OAuth2RefreshTokenEntity() { + // TODO Auto-generated constructor stub + } + + /* (non-Javadoc) + * @see org.springframework.security.oauth2.common.OAuth2RefreshToken#getValue() + */ + @Override + @Id + @Column(name="id") + public String getValue() { + // TODO Auto-generated method stub + return super.getValue(); + } + + /* (non-Javadoc) + * @see org.springframework.security.oauth2.common.OAuth2RefreshToken#setValue(java.lang.String) + */ + @Override + public void setValue(String value) { + // TODO Auto-generated method stub + super.setValue(value); + } + + /* (non-Javadoc) + * @see org.springframework.security.oauth2.common.ExpiringOAuth2RefreshToken#getExpiration() + */ + @Override + @Basic + @Temporal(javax.persistence.TemporalType.TIMESTAMP) + public Date getExpiration() { + // TODO Auto-generated method stub + return super.getExpiration(); + } + + /* (non-Javadoc) + * @see org.springframework.security.oauth2.common.ExpiringOAuth2RefreshToken#setExpiration(java.util.Date) + */ + @Override + public void setExpiration(Date expiration) { + // TODO Auto-generated method stub + super.setExpiration(expiration); + } + + /** + * Has this token expired? + * @return true if it has a timeout set and the timeout has passed + */ + @Transient + public boolean isExpired() { + return getExpiration() == null ? false : System.currentTimeMillis() > getExpiration().getTime(); + } + + /** + * @return the client + */ + @ManyToOne(fetch = FetchType.EAGER) + @JoinColumn(name = "client_id") + public ClientDetailsEntity getClient() { + return client; + } + + + /** + * @param client the client to set + */ + public void setClient(ClientDetailsEntity client) { + this.client = client; + } + + /** + * @return the scope + */ + @ElementCollection(fetch = FetchType.EAGER) + @CollectionTable( + joinColumns=@JoinColumn(name="owner_id"), + name="scope" + ) + public Set getScope() { + return scope; + } + + /** + * @param scope the scope to set + */ + public void setScope(Set scope) { + this.scope = scope; + } + + + +} diff --git a/server/src/main/java/org/mitre/oauth2/model/OAuth2RefreshTokenEntityFactory.java b/server/src/main/java/org/mitre/oauth2/model/OAuth2RefreshTokenEntityFactory.java new file mode 100644 index 000000000..2a6ac4785 --- /dev/null +++ b/server/src/main/java/org/mitre/oauth2/model/OAuth2RefreshTokenEntityFactory.java @@ -0,0 +1,7 @@ +package org.mitre.oauth2.model; + +public interface OAuth2RefreshTokenEntityFactory { + + public OAuth2RefreshTokenEntity createNewRefreshToken(); + +} diff --git a/server/src/main/java/org/mitre/oauth2/model/UUIDTokenFactory.java b/server/src/main/java/org/mitre/oauth2/model/UUIDTokenFactory.java new file mode 100644 index 000000000..91f93856a --- /dev/null +++ b/server/src/main/java/org/mitre/oauth2/model/UUIDTokenFactory.java @@ -0,0 +1,39 @@ +package org.mitre.oauth2.model; + +import java.util.UUID; + +import org.springframework.stereotype.Service; + +@Service +public class UUIDTokenFactory implements OAuth2AccessTokenEntityFactory, OAuth2RefreshTokenEntityFactory { + + /** + * Create a new access token and set its value to a random UUID + */ + @Override + public OAuth2AccessTokenEntity createNewAccessToken() { + // create our token container + OAuth2AccessTokenEntity token = new OAuth2AccessTokenEntity(); + + // set a random value (TODO: support JWT) + String tokenValue = UUID.randomUUID().toString(); + token.setValue(tokenValue); + + return token; + } + + /** + * Create a new refresh token and set its value to a random UUID + */ + @Override + public OAuth2RefreshTokenEntity createNewRefreshToken() { + OAuth2RefreshTokenEntity refreshToken = new OAuth2RefreshTokenEntity(); + + // set a random value for the refresh + String refreshTokenValue = UUID.randomUUID().toString(); + refreshToken.setValue(refreshTokenValue); + + return refreshToken; + } + +} diff --git a/server/src/main/java/org/mitre/oauth2/model/serializer/JSONOAuthClientView.java b/server/src/main/java/org/mitre/oauth2/model/serializer/JSONOAuthClientView.java new file mode 100644 index 000000000..843d7e3ac --- /dev/null +++ b/server/src/main/java/org/mitre/oauth2/model/serializer/JSONOAuthClientView.java @@ -0,0 +1,66 @@ +package org.mitre.oauth2.model.serializer; + +import java.io.Writer; +import java.lang.reflect.Type; +import java.util.Map; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.security.core.GrantedAuthority; +import org.springframework.validation.BeanPropertyBindingResult; +import org.springframework.web.servlet.view.AbstractView; + +import com.google.gson.ExclusionStrategy; +import com.google.gson.FieldAttributes; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonElement; +import com.google.gson.JsonPrimitive; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; + +public class JSONOAuthClientView extends AbstractView { + + @Override + protected void renderMergedOutputModel(Map model, HttpServletRequest request, HttpServletResponse response) throws Exception { + Gson gson = new GsonBuilder().setExclusionStrategies(new ExclusionStrategy() { + + @Override + public boolean shouldSkipField(FieldAttributes f) { + return false; + } + + @Override + public boolean shouldSkipClass(Class clazz) { + // skip the JPA binding wrapper + if (clazz.equals(BeanPropertyBindingResult.class)) { + return true; + } else { + return false; + } + } + + }) + .registerTypeAdapter(GrantedAuthority.class, new JsonSerializer() { + @Override + public JsonElement serialize(GrantedAuthority src, Type typeOfSrc, JsonSerializationContext context) { + return new JsonPrimitive(src.getAuthority()); + } + }) + .create(); + + response.setContentType("application/json"); + + Writer out = response.getWriter(); + + Object obj = model.get("entity"); + if (obj == null) { + obj = model; + } + + gson.toJson(obj, out); + + } + +} diff --git a/server/src/main/java/org/mitre/oauth2/model/serializer/TokenIntrospection.java b/server/src/main/java/org/mitre/oauth2/model/serializer/TokenIntrospection.java new file mode 100644 index 000000000..28af3d3f2 --- /dev/null +++ b/server/src/main/java/org/mitre/oauth2/model/serializer/TokenIntrospection.java @@ -0,0 +1,100 @@ +package org.mitre.oauth2.model.serializer; + +import java.io.Writer; +import java.lang.reflect.Type; +import java.text.DateFormat; +import java.util.Map; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.mitre.oauth2.model.OAuth2AccessTokenEntity; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.validation.BeanPropertyBindingResult; +import org.springframework.web.servlet.view.AbstractView; + +import com.google.gson.ExclusionStrategy; +import com.google.gson.FieldAttributes; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; + +public class TokenIntrospection extends AbstractView { + + @Override + protected void renderMergedOutputModel(Map model, HttpServletRequest request, HttpServletResponse response) throws Exception { + + Gson gson = new GsonBuilder().setExclusionStrategies(new ExclusionStrategy() { + + @Override + public boolean shouldSkipField(FieldAttributes f) { + /* + if (f.getDeclaringClass().isAssignableFrom(OAuth2AccessTokenEntity.class)) { + // we don't want to serialize the whole object, just the scope and timeout + if (f.getName().equals("scope")) { + return false; + } else if (f.getName().equals("expiration")) { + return false; + } else { + // skip everything else on this class + return true; + } + } else { + // serialize other classes without filter (lists and sets and things) + return false; + } + */ + return false; + } + + @Override + public boolean shouldSkipClass(Class clazz) { + // skip the JPA binding wrapper + if (clazz.equals(BeanPropertyBindingResult.class)) { + return true; + } else { + return false; + } + } + + }) + .registerTypeAdapter(OAuth2AccessTokenEntity.class, new JsonSerializer() { + public JsonElement serialize(OAuth2AccessTokenEntity src, Type typeOfSrc, JsonSerializationContext context) { + JsonObject token = new JsonObject(); + + token.addProperty("valid", true); + + JsonArray scopes = new JsonArray(); + for (String scope : src.getScope()) { + scopes.add(new JsonPrimitive(scope)); + } + token.add("scope", scopes); + + token.add("expires", context.serialize(src.getExpiration())); + + return token; + } + + }) + .setDateFormat("yyyy-MM-dd'T'HH:mm:ssZ") + .create(); + + response.setContentType("application/json"); + + Writer out = response.getWriter(); + + Object obj = model.get("entity"); + if (obj == null) { + obj = model; + } + + gson.toJson(obj, out); + + } + +} diff --git a/server/src/main/java/org/mitre/oauth2/repository/OAuth2ClientRepository.java b/server/src/main/java/org/mitre/oauth2/repository/OAuth2ClientRepository.java new file mode 100644 index 000000000..cd9de162f --- /dev/null +++ b/server/src/main/java/org/mitre/oauth2/repository/OAuth2ClientRepository.java @@ -0,0 +1,22 @@ +package org.mitre.oauth2.repository; + +import java.util.Collection; +import java.util.List; + +import org.mitre.oauth2.model.ClientDetailsEntity; +import org.springframework.security.oauth2.provider.ClientDetails; +import org.springframework.stereotype.Repository; + +public interface OAuth2ClientRepository { + + public ClientDetailsEntity getClientById(String clientId); + + public ClientDetailsEntity saveClient(ClientDetailsEntity client); + + public void deleteClient(ClientDetailsEntity client); + + public ClientDetailsEntity updateClient(String clientId, ClientDetailsEntity client); + + public Collection getAllClients(); + +} diff --git a/server/src/main/java/org/mitre/oauth2/repository/OAuth2TokenRepository.java b/server/src/main/java/org/mitre/oauth2/repository/OAuth2TokenRepository.java new file mode 100644 index 000000000..475a5c0c3 --- /dev/null +++ b/server/src/main/java/org/mitre/oauth2/repository/OAuth2TokenRepository.java @@ -0,0 +1,35 @@ +package org.mitre.oauth2.repository; + +import java.util.List; + +import org.mitre.oauth2.model.ClientDetailsEntity; +import org.mitre.oauth2.model.OAuth2AccessTokenEntity; +import org.mitre.oauth2.model.OAuth2RefreshTokenEntity; + +public interface OAuth2TokenRepository { + + public OAuth2AccessTokenEntity saveAccessToken(OAuth2AccessTokenEntity token); + + public OAuth2RefreshTokenEntity getRefreshTokenByValue(String refreshTokenValue); + + public void clearAccessTokensForRefreshToken(OAuth2RefreshTokenEntity refreshToken); + + public void removeRefreshToken(OAuth2RefreshTokenEntity refreshToken); + + public OAuth2RefreshTokenEntity saveRefreshToken(OAuth2RefreshTokenEntity refreshToken); + + public OAuth2AccessTokenEntity getAccessTokenByValue(String accessTokenValue); + + public void removeAccessToken(OAuth2AccessTokenEntity accessToken); + + public void clearTokensForClient(ClientDetailsEntity client); + + public List getAccessTokensForClient(ClientDetailsEntity client); + + public List getRefreshTokensForClient(ClientDetailsEntity client); + + public List getExpiredAccessTokens(); + + public List getExpiredRefreshTokens(); + +} diff --git a/server/src/main/java/org/mitre/oauth2/repository/impl/JpaOAuth2ClientRepository.java b/server/src/main/java/org/mitre/oauth2/repository/impl/JpaOAuth2ClientRepository.java new file mode 100644 index 000000000..7022270bb --- /dev/null +++ b/server/src/main/java/org/mitre/oauth2/repository/impl/JpaOAuth2ClientRepository.java @@ -0,0 +1,74 @@ +package org.mitre.oauth2.repository.impl; + +import java.util.Collection; + +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; +import javax.persistence.TypedQuery; + +import org.mitre.oauth2.model.ClientDetailsEntity; +import org.mitre.oauth2.repository.OAuth2ClientRepository; +import org.mitre.util.jpa.JpaUtil; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; + +/** + * @author jricher + * + */ +@Repository +@Transactional +public class JpaOAuth2ClientRepository implements OAuth2ClientRepository { + + @PersistenceContext + private EntityManager manager; + + public JpaOAuth2ClientRepository() { + + } + + public JpaOAuth2ClientRepository(EntityManager manager) { + this.manager = manager; + } + + /* (non-Javadoc) + * @see org.mitre.oauth2.repository.OAuth2ClientRepository#getClientById(java.lang.String) + */ + @Override + public ClientDetailsEntity getClientById(String clientId) { + return manager.find(ClientDetailsEntity.class, clientId); + } + + /* (non-Javadoc) + * @see org.mitre.oauth2.repository.OAuth2ClientRepository#saveClient(org.mitre.oauth2.model.ClientDetailsEntity) + */ + @Override + public ClientDetailsEntity saveClient(ClientDetailsEntity client) { + return JpaUtil.saveOrUpdate(client.getClientId(), manager, client); + } + + /* (non-Javadoc) + * @see org.mitre.oauth2.repository.OAuth2ClientRepository#deleteClient(org.mitre.oauth2.model.ClientDetailsEntity) + */ + @Override + public void deleteClient(ClientDetailsEntity client) { + ClientDetailsEntity found = getClientById(client.getClientId()); + if (found != null) { + manager.remove(found); + } else { + throw new IllegalArgumentException("Client not found: " + client); + } + } + + @Override + public ClientDetailsEntity updateClient(String clientId, ClientDetailsEntity client) { + return JpaUtil.saveOrUpdate(clientId, manager, client); + } + + @Override + public Collection getAllClients() { + TypedQuery query = manager.createNamedQuery("ClientDetailsEntity.findAll", ClientDetailsEntity.class); + return query.getResultList(); + } + +} diff --git a/server/src/main/java/org/mitre/oauth2/repository/impl/JpaOAuth2TokenRepository.java b/server/src/main/java/org/mitre/oauth2/repository/impl/JpaOAuth2TokenRepository.java new file mode 100644 index 000000000..85cd9041d --- /dev/null +++ b/server/src/main/java/org/mitre/oauth2/repository/impl/JpaOAuth2TokenRepository.java @@ -0,0 +1,138 @@ +package org.mitre.oauth2.repository.impl; + +import java.util.List; + +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; +import javax.persistence.TypedQuery; + +import org.mitre.oauth2.model.ClientDetailsEntity; +import org.mitre.oauth2.model.OAuth2AccessTokenEntity; +import org.mitre.oauth2.model.OAuth2RefreshTokenEntity; +import org.mitre.oauth2.repository.OAuth2TokenRepository; +import org.mitre.util.jpa.JpaUtil; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; + +@Repository +@Transactional +public class JpaOAuth2TokenRepository implements OAuth2TokenRepository { + + @PersistenceContext + private EntityManager manager; + + @Override + public OAuth2AccessTokenEntity getAccessTokenByValue(String accessTokenValue) { + return manager.find(OAuth2AccessTokenEntity.class, accessTokenValue); + } + + @Override + @Transactional + public OAuth2AccessTokenEntity saveAccessToken(OAuth2AccessTokenEntity token) { + return JpaUtil.saveOrUpdate(token.getValue(), manager, token); + } + + @Override + @Transactional + public void removeAccessToken(OAuth2AccessTokenEntity accessToken) { + OAuth2AccessTokenEntity found = getAccessTokenByValue(accessToken.getValue()); + if (found != null) { + manager.remove(found); + } else { + throw new IllegalArgumentException("Access token not found: " + accessToken); + } + } + + @Override + @Transactional + public void clearAccessTokensForRefreshToken(OAuth2RefreshTokenEntity refreshToken) { + TypedQuery query = manager.createNamedQuery("OAuth2AccessTokenEntity.getByRefreshToken", OAuth2AccessTokenEntity.class); + query.setParameter("refreshToken", refreshToken); + List accessTokens = query.getResultList(); + for (OAuth2AccessTokenEntity accessToken : accessTokens) { + removeAccessToken(accessToken); + } + } + + @Override + public OAuth2RefreshTokenEntity getRefreshTokenByValue(String refreshTokenValue) { + return manager.find(OAuth2RefreshTokenEntity.class, refreshTokenValue); + } + + @Override + @Transactional + public OAuth2RefreshTokenEntity saveRefreshToken(OAuth2RefreshTokenEntity refreshToken) { + return JpaUtil.saveOrUpdate(refreshToken.getValue(), manager, refreshToken); + } + + @Override + @Transactional + public void removeRefreshToken(OAuth2RefreshTokenEntity refreshToken) { + OAuth2RefreshTokenEntity found = getRefreshTokenByValue(refreshToken.getValue()); + if (found != null) { + manager.remove(found); + } else { + throw new IllegalArgumentException("Refresh token not found: " + refreshToken); + } + } + + @Override + @Transactional + public void clearTokensForClient(ClientDetailsEntity client) { + TypedQuery queryA = manager.createNamedQuery("OAuth2AccessTokenEntity.getByClient", OAuth2AccessTokenEntity.class); + queryA.setParameter("client", client); + List accessTokens = queryA.getResultList(); + for (OAuth2AccessTokenEntity accessToken : accessTokens) { + removeAccessToken(accessToken); + } + TypedQuery queryR = manager.createNamedQuery("OAuth2RefreshTokenEntity.getByClient", OAuth2RefreshTokenEntity.class); + queryR.setParameter("client", client); + List refreshTokens = queryR.getResultList(); + for (OAuth2RefreshTokenEntity refreshToken : refreshTokens) { + removeRefreshToken(refreshToken); + } + } + + /* (non-Javadoc) + * @see org.mitre.oauth2.repository.OAuth2TokenRepository#getAccessTokensForClient(org.mitre.oauth2.model.ClientDetailsEntity) + */ + @Override + public List getAccessTokensForClient(ClientDetailsEntity client) { + TypedQuery queryA = manager.createNamedQuery("OAuth2AccessTokenEntity.getByClient", OAuth2AccessTokenEntity.class); + queryA.setParameter("client", client); + List accessTokens = queryA.getResultList(); + return accessTokens; + } + + /* (non-Javadoc) + * @see org.mitre.oauth2.repository.OAuth2TokenRepository#getRefreshTokensForClient(org.mitre.oauth2.model.ClientDetailsEntity) + */ + @Override + public List getRefreshTokensForClient(ClientDetailsEntity client) { + TypedQuery queryR = manager.createNamedQuery("OAuth2RefreshTokenEntity.getByClient", OAuth2RefreshTokenEntity.class); + queryR.setParameter("client", client); + List refreshTokens = queryR.getResultList(); + return refreshTokens; + } + + /* (non-Javadoc) + * @see org.mitre.oauth2.repository.OAuth2TokenRepository#getExpiredAccessTokens() + */ + @Override + public List getExpiredAccessTokens() { + TypedQuery queryA = manager.createNamedQuery("OAuth2AccessTokenEntity.getExpired", OAuth2AccessTokenEntity.class); + List accessTokens = queryA.getResultList(); + return accessTokens; + } + + /* (non-Javadoc) + * @see org.mitre.oauth2.repository.OAuth2TokenRepository#getExpiredRefreshTokens() + */ + @Override + public List getExpiredRefreshTokens() { + TypedQuery queryR = manager.createNamedQuery("OAuth2RefreshTokenEntity.getExpired", OAuth2RefreshTokenEntity.class); + List refreshTokens = queryR.getResultList(); + return refreshTokens; + } + +} diff --git a/server/src/main/java/org/mitre/oauth2/service/ClientDetailsEntityService.java b/server/src/main/java/org/mitre/oauth2/service/ClientDetailsEntityService.java new file mode 100644 index 000000000..d1c9757ad --- /dev/null +++ b/server/src/main/java/org/mitre/oauth2/service/ClientDetailsEntityService.java @@ -0,0 +1,22 @@ +package org.mitre.oauth2.service; + +import java.util.Collection; +import java.util.Set; + +import org.mitre.oauth2.model.ClientDetailsEntity; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.oauth2.common.exceptions.OAuth2Exception; +import org.springframework.security.oauth2.provider.ClientDetailsService; + +public interface ClientDetailsEntityService extends ClientDetailsService { + + public ClientDetailsEntity loadClientByClientId(String clientId) throws OAuth2Exception; + + public ClientDetailsEntity createClient(String clientId, String clientSecret, Set scope, Set grantTypes, String redirectUri, Set authorities, Set resourceIds, String name, String description, boolean allowRefresh, Long accessTokenTimeout, Long refreshTokenTimeout, String owner); + + public void deleteClient(ClientDetailsEntity client); + + public ClientDetailsEntity updateClient(ClientDetailsEntity oldClient, ClientDetailsEntity newClient); + + public Collection getAllClients(); +} diff --git a/server/src/main/java/org/mitre/oauth2/service/OAuth2TokenEntityService.java b/server/src/main/java/org/mitre/oauth2/service/OAuth2TokenEntityService.java new file mode 100644 index 000000000..724a4cd8d --- /dev/null +++ b/server/src/main/java/org/mitre/oauth2/service/OAuth2TokenEntityService.java @@ -0,0 +1,26 @@ +package org.mitre.oauth2.service; + +import java.util.List; + +import org.mitre.oauth2.model.ClientDetailsEntity; +import org.mitre.oauth2.model.OAuth2AccessTokenEntity; +import org.mitre.oauth2.model.OAuth2RefreshTokenEntity; +import org.springframework.security.oauth2.provider.token.AuthorizationServerTokenServices; +import org.springframework.security.oauth2.provider.token.ResourceServerTokenServices; + +public interface OAuth2TokenEntityService extends AuthorizationServerTokenServices, ResourceServerTokenServices { + + public OAuth2AccessTokenEntity getAccessToken(String accessTokenValue); + + public OAuth2RefreshTokenEntity getRefreshToken(String refreshTokenValue); + + public void revokeRefreshToken(OAuth2RefreshTokenEntity refreshToken); + + public void revokeAccessToken(OAuth2AccessTokenEntity accessToken); + + public List getAccessTokensForClient(ClientDetailsEntity client); + + public List getRefreshTokensForClient(ClientDetailsEntity client); + + public void clearExpiredTokens(); +} diff --git a/server/src/main/java/org/mitre/oauth2/service/impl/DefaultOAuth2ClientDetailsEntityService.java b/server/src/main/java/org/mitre/oauth2/service/impl/DefaultOAuth2ClientDetailsEntityService.java new file mode 100644 index 000000000..d865ac4fa --- /dev/null +++ b/server/src/main/java/org/mitre/oauth2/service/impl/DefaultOAuth2ClientDetailsEntityService.java @@ -0,0 +1,130 @@ +package org.mitre.oauth2.service.impl; + +import java.util.Collection; +import java.util.List; +import java.util.Set; + +import org.mitre.oauth2.model.ClientDetailsEntity; +import org.mitre.oauth2.model.ClientDetailsEntityFactory; +import org.mitre.oauth2.repository.OAuth2ClientRepository; +import org.mitre.oauth2.repository.OAuth2TokenRepository; +import org.mitre.oauth2.service.ClientDetailsEntityService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.oauth2.common.exceptions.InvalidClientException; +import org.springframework.security.oauth2.common.exceptions.OAuth2Exception; +import org.springframework.stereotype.Service; + +import com.google.common.base.Strings; + +@Service +public class DefaultOAuth2ClientDetailsEntityService implements ClientDetailsEntityService { + + @Autowired + private OAuth2ClientRepository clientRepository; + + @Autowired + private OAuth2TokenRepository tokenRepository; + + @Autowired + private ClientDetailsEntityFactory clientFactory; + + public DefaultOAuth2ClientDetailsEntityService() { + + } + + public DefaultOAuth2ClientDetailsEntityService(OAuth2ClientRepository clientRepository, + OAuth2TokenRepository tokenRepository, ClientDetailsEntityFactory clientFactory) { + this.clientRepository = clientRepository; + this.tokenRepository = tokenRepository; + this.clientFactory = clientFactory; + } + + /** + * Get the client for the given ID + */ + @Override + public ClientDetailsEntity loadClientByClientId(String clientId) throws OAuth2Exception, InvalidClientException, IllegalArgumentException { + if (!Strings.isNullOrEmpty(clientId)) { + ClientDetailsEntity client = clientRepository.getClientById(clientId); + if (client == null) { + throw new InvalidClientException("Client with id " + clientId + " was not found"); + } + else { + return client; + } + } + + throw new IllegalArgumentException("Client id must not be empty!"); + } + + /** + * Create a new client with the appropriate fields filled in + */ + @Override + public ClientDetailsEntity createClient(String clientId, String clientSecret, + Set scope, Set grantTypes, String redirectUri, Set authorities, + Set resourceIds, + String name, String description, boolean allowRefresh, Long accessTokenTimeout, + Long refreshTokenTimeout, String owner) { + + // TODO: check "owner" locally? + + ClientDetailsEntity client = clientFactory.createClient(clientId, clientSecret); + client.setScope(scope); + client.setAuthorizedGrantTypes(grantTypes); + client.setRegisteredRedirectUri(redirectUri); + client.setAuthorities(authorities); + client.setClientName(name); + client.setClientDescription(description); + client.setAllowRefresh(allowRefresh); + client.setAccessTokenTimeout(accessTokenTimeout); + client.setRefreshTokenTimeout(refreshTokenTimeout); + client.setResourceIds(resourceIds); + client.setOwner(owner); + + clientRepository.saveClient(client); + + return client; + + } + + /** + * Delete a client and all its associated tokens + */ + @Override + public void deleteClient(ClientDetailsEntity client) throws InvalidClientException { + + if (clientRepository.getClientById(client.getClientId()) == null) { + throw new InvalidClientException("Client with id " + client.getClientId() + " was not found"); + } + + // clean out any tokens that this client had issued + tokenRepository.clearTokensForClient(client); + + // take care of the client itself + clientRepository.deleteClient(client); + + } + + /** + * Update the oldClient with information from the newClient. The + * id from oldClient is retained. + */ + @Override + public ClientDetailsEntity updateClient(ClientDetailsEntity oldClient, ClientDetailsEntity newClient) throws IllegalArgumentException { + if (oldClient != null && newClient != null) { + return clientRepository.updateClient(oldClient.getClientId(), newClient); + } + throw new IllegalArgumentException("Neither old client or new client can be null!"); + } + + /** + * Get all clients in the system + */ + @Override + public Collection getAllClients() { + return clientRepository.getAllClients(); + } + +} diff --git a/server/src/main/java/org/mitre/oauth2/service/impl/DefaultOAuth2ProviderTokenService.java b/server/src/main/java/org/mitre/oauth2/service/impl/DefaultOAuth2ProviderTokenService.java new file mode 100644 index 000000000..3b71b7318 --- /dev/null +++ b/server/src/main/java/org/mitre/oauth2/service/impl/DefaultOAuth2ProviderTokenService.java @@ -0,0 +1,319 @@ +/** + * + */ +package org.mitre.oauth2.service.impl; + +import java.util.Date; +import java.util.List; +import java.util.Set; + +import org.mitre.oauth2.model.ClientDetailsEntity; +import org.mitre.oauth2.model.OAuth2AccessTokenEntity; +import org.mitre.oauth2.model.OAuth2AccessTokenEntityFactory; +import org.mitre.oauth2.model.OAuth2RefreshTokenEntity; +import org.mitre.oauth2.model.OAuth2RefreshTokenEntityFactory; +import org.mitre.oauth2.repository.OAuth2TokenRepository; +import org.mitre.oauth2.service.ClientDetailsEntityService; +import org.mitre.oauth2.service.OAuth2TokenEntityService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.oauth2.common.exceptions.InvalidClientException; +import org.springframework.security.oauth2.common.exceptions.InvalidTokenException; +import org.springframework.security.oauth2.provider.AuthorizationRequest; +import org.springframework.security.oauth2.provider.OAuth2Authentication; +import org.springframework.security.oauth2.provider.client.ClientAuthenticationToken; +import org.springframework.stereotype.Service; + +import com.google.common.collect.Sets; + + +/** + * @author jricher + * + */ +@Service +public class DefaultOAuth2ProviderTokenService implements OAuth2TokenEntityService { + + private static Logger logger = LoggerFactory.getLogger(DefaultOAuth2ProviderTokenService.class); + + @Autowired + private OAuth2TokenRepository tokenRepository; + + @Autowired + private ClientDetailsEntityService clientDetailsService; + + @Autowired + private OAuth2AccessTokenEntityFactory accessTokenFactory; + + @Autowired + private OAuth2RefreshTokenEntityFactory refreshTokenFactory; + + @Override + public OAuth2AccessTokenEntity createAccessToken(OAuth2Authentication authentication) throws AuthenticationException, InvalidClientException { + if (authentication != null && + authentication.getAuthorizationRequest() != null) { + // look up our client + AuthorizationRequest clientAuth = authentication.getAuthorizationRequest(); + + ClientDetailsEntity client = clientDetailsService.loadClientByClientId(clientAuth.getClientId()); + + if (client == null) { + throw new InvalidClientException("Client not found: " + clientAuth.getClientId()); + } + + OAuth2AccessTokenEntity token = accessTokenFactory.createNewAccessToken(); + + // attach the client + token.setClient(client); + + // inherit the scope from the auth + // this lets us match which scope is requested + if (client.isScoped()) { + + // restrict granted scopes to a valid subset of those + Set validScopes = Sets.newHashSet(); + + for (String requested : clientAuth.getScope()) { + if (client.getScope().contains(requested)) { + validScopes.add(requested); + } else { + logger.warn("Client " + client.getClientId() + " requested out of permission scope: " + requested); + } + } + + token.setScope(validScopes); + } + + // make it expire if necessary + if (client.getAccessTokenTimeout() != null) { + Date expiration = new Date(System.currentTimeMillis() + (client.getAccessTokenTimeout() * 1000L)); + token.setExpiration(expiration); + } + + // attach the authorization so that we can look it up later + token.setAuthentication(authentication); + + // attach a refresh token, if this client is allowed to request them + if (client.isAllowRefresh()) { + OAuth2RefreshTokenEntity refreshToken = refreshTokenFactory.createNewRefreshToken(); + + // make it expire if necessary + if (client.getRefreshTokenTimeout() != null) { + Date expiration = new Date(System.currentTimeMillis() + (client.getRefreshTokenTimeout() * 1000L)); + refreshToken.setExpiration(expiration); + } + + // save our scopes so that we can reuse them later for more auth tokens + // TODO: save the auth instead of the just the scope? + if (client.isScoped()) { + refreshToken.setScope(clientAuth.getScope()); + } + + tokenRepository.saveRefreshToken(refreshToken); + + token.setRefreshToken(refreshToken); + } + + tokenRepository.saveAccessToken(token); + + return token; + } + + throw new AuthenticationCredentialsNotFoundException("No authentication credentials found"); + } + + @Override + public OAuth2AccessTokenEntity refreshAccessToken(String refreshTokenValue, Set scope) throws AuthenticationException { + + OAuth2RefreshTokenEntity refreshToken = tokenRepository.getRefreshTokenByValue(refreshTokenValue); + + if (refreshToken == null) { + throw new InvalidTokenException("Invalid refresh token: " + refreshTokenValue); + } + + ClientDetailsEntity client = refreshToken.getClient(); + + //Make sure this client allows access token refreshing + if (!client.isAllowRefresh()) { + throw new InvalidClientException("Client does not allow refreshing access token!"); + } + + // clear out any access tokens + // TODO: make this a configurable option + tokenRepository.clearAccessTokensForRefreshToken(refreshToken); + + if (refreshToken.isExpired()) { + tokenRepository.removeRefreshToken(refreshToken); + throw new InvalidTokenException("Expired refresh token: " + refreshTokenValue); + } + + // TODO: have the option to recycle the refresh token here, too + // for now, we just reuse it as long as it's valid, which is the original intent + + OAuth2AccessTokenEntity token = accessTokenFactory.createNewAccessToken(); + + + if (scope != null && !scope.isEmpty()) { + // ensure a proper subset of scopes + if (refreshToken.getScope() != null && refreshToken.getScope().containsAll(scope)) { + // set the scope of the new access token if requested + refreshToken.setScope(scope); + } else { + // up-scoping is not allowed + // (TODO: should this throw InvalidScopeException? For now just pass through) + token.setScope(refreshToken.getScope()); + } + } else { + // otherwise inherit the scope of the refresh token (if it's there -- this can return a null scope set) + token.setScope(refreshToken.getScope()); + } + + token.setClient(client); + + if (client.getAccessTokenTimeout() != null) { + Date expiration = new Date(System.currentTimeMillis() + (client.getAccessTokenTimeout() * 1000L)); + token.setExpiration(expiration); + } + + token.setRefreshToken(refreshToken); + + tokenRepository.saveAccessToken(token); + + return token; + + } + + @Override + public OAuth2Authentication loadAuthentication(String accessTokenValue) throws AuthenticationException { + + OAuth2AccessTokenEntity accessToken = tokenRepository.getAccessTokenByValue(accessTokenValue); + + if (accessToken == null) { + throw new InvalidTokenException("Invalid access token: " + accessTokenValue); + } + + if (accessToken.isExpired()) { + tokenRepository.removeAccessToken(accessToken); + throw new InvalidTokenException("Expired access token: " + accessTokenValue); + } + + return accessToken.getAuthentication(); + } + + + @Override + public OAuth2AccessTokenEntity getAccessToken(String accessTokenValue) throws AuthenticationException { + OAuth2AccessTokenEntity accessToken = tokenRepository.getAccessTokenByValue(accessTokenValue); + if (accessToken == null) { + throw new InvalidTokenException("Access token for value " + accessTokenValue + " was not found"); + } + else { + return accessToken; + } + } + + @Override + public OAuth2RefreshTokenEntity getRefreshToken(String refreshTokenValue) throws AuthenticationException { + OAuth2RefreshTokenEntity refreshToken = tokenRepository.getRefreshTokenByValue(refreshTokenValue); + if (refreshToken == null) { + throw new InvalidTokenException("Refresh token for value " + refreshTokenValue + " was not found"); + } + else { + return refreshToken; + } + } + + @Override + public void revokeRefreshToken(OAuth2RefreshTokenEntity refreshToken) { + tokenRepository.clearAccessTokensForRefreshToken(refreshToken); + tokenRepository.removeRefreshToken(refreshToken); + } + + @Override + public void revokeAccessToken(OAuth2AccessTokenEntity accessToken) { + tokenRepository.removeAccessToken(accessToken); + } + + + /* (non-Javadoc) + * @see org.mitre.oauth2.service.OAuth2TokenEntityService#getAccessTokensForClient(org.mitre.oauth2.model.ClientDetailsEntity) + */ + @Override + public List getAccessTokensForClient(ClientDetailsEntity client) { + return tokenRepository.getAccessTokensForClient(client); + } + + /* (non-Javadoc) + * @see org.mitre.oauth2.service.OAuth2TokenEntityService#getRefreshTokensForClient(org.mitre.oauth2.model.ClientDetailsEntity) + */ + @Override + public List getRefreshTokensForClient(ClientDetailsEntity client) { + return tokenRepository.getRefreshTokensForClient(client); + } + + @Override + @Scheduled(fixedRate = 5 * 60 * 1000) // schedule this task every five minutes + public void clearExpiredTokens() { + logger.info("Cleaning out all expired tokens"); + + List accessTokens = tokenRepository.getExpiredAccessTokens(); + logger.info("Found " + accessTokens.size() + " expired access tokens"); + for (OAuth2AccessTokenEntity oAuth2AccessTokenEntity : accessTokens) { + revokeAccessToken(oAuth2AccessTokenEntity); + } + + List refreshTokens = tokenRepository.getExpiredRefreshTokens(); + logger.info("Found " + refreshTokens.size() + " expired refresh tokens"); + for (OAuth2RefreshTokenEntity oAuth2RefreshTokenEntity : refreshTokens) { + revokeRefreshToken(oAuth2RefreshTokenEntity); + } + } + + /** + * Get a builder object for this class (for tests) + * @return + */ + public static DefaultOAuth2ProviderTokenServicesBuilder makeBuilder() { + return new DefaultOAuth2ProviderTokenServicesBuilder(); + } + + /** + * Builder class for test harnesses. + */ + public static class DefaultOAuth2ProviderTokenServicesBuilder { + private DefaultOAuth2ProviderTokenService instance; + + private DefaultOAuth2ProviderTokenServicesBuilder() { + instance = new DefaultOAuth2ProviderTokenService(); + } + + public DefaultOAuth2ProviderTokenServicesBuilder setTokenRepository(OAuth2TokenRepository tokenRepository) { + instance.tokenRepository = tokenRepository; + return this; + } + + public DefaultOAuth2ProviderTokenServicesBuilder setClientDetailsService(ClientDetailsEntityService clientDetailsService) { + instance.clientDetailsService = clientDetailsService; + return this; + } + + public DefaultOAuth2ProviderTokenServicesBuilder setAccessTokenFactory(OAuth2AccessTokenEntityFactory accessTokenFactory) { + instance.accessTokenFactory = accessTokenFactory; + return this; + } + + public DefaultOAuth2ProviderTokenServicesBuilder setRefreshTokenFactory(OAuth2RefreshTokenEntityFactory refreshTokenFactory) { + instance.refreshTokenFactory = refreshTokenFactory; + return this; + } + + public OAuth2TokenEntityService finish() { + return instance; + } + } + +} diff --git a/server/src/main/java/org/mitre/oauth2/web/IntrospectionEndpoint.java b/server/src/main/java/org/mitre/oauth2/web/IntrospectionEndpoint.java new file mode 100644 index 000000000..e47490c39 --- /dev/null +++ b/server/src/main/java/org/mitre/oauth2/web/IntrospectionEndpoint.java @@ -0,0 +1,43 @@ +package org.mitre.oauth2.web; + +import org.mitre.oauth2.model.OAuth2AccessTokenEntity; +import org.mitre.oauth2.service.OAuth2TokenEntityService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.servlet.ModelAndView; + +@Controller +public class IntrospectionEndpoint { + + @Autowired + OAuth2TokenEntityService tokenServices; + + public IntrospectionEndpoint() { + + } + + public IntrospectionEndpoint(OAuth2TokenEntityService tokenServices) { + this.tokenServices = tokenServices; + } + + // TODO + @RequestMapping("/oauth/verify") + public ModelAndView verify(@RequestParam("token") String tokenValue, + ModelAndView modelAndView) { + OAuth2AccessTokenEntity token = tokenServices.getAccessToken(tokenValue); + + if (token == null) { + // if it's not a valid token, we'll print a 404 + modelAndView.setViewName("tokenNotFound"); + } else { + // if it's a valid token, we'll print out the scope and expiration + modelAndView.setViewName("tokenIntrospection"); + modelAndView.addObject("entity", token); + } + + return modelAndView; + } + +} diff --git a/server/src/main/java/org/mitre/oauth2/web/OAuthClientAPI.java b/server/src/main/java/org/mitre/oauth2/web/OAuthClientAPI.java new file mode 100644 index 000000000..8182df153 --- /dev/null +++ b/server/src/main/java/org/mitre/oauth2/web/OAuthClientAPI.java @@ -0,0 +1,201 @@ +package org.mitre.oauth2.web; + +import java.util.Collection; +import java.util.List; +import java.util.Set; + +import org.mitre.oauth2.exception.ClientNotFoundException; +import org.mitre.oauth2.exception.DuplicateClientIdException; +import org.mitre.oauth2.model.ClientDetailsEntity; +import org.mitre.oauth2.service.ClientDetailsEntityService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.GrantedAuthorityImpl; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.servlet.ModelAndView; + +import com.google.common.base.Function; +import com.google.common.base.Splitter; +import com.google.common.collect.Iterables; +import com.google.common.collect.Lists; +import com.google.common.collect.Sets; + +@Controller +@RequestMapping("/manager/oauth/clients/api") +public class OAuthClientAPI { + + @Autowired + private ClientDetailsEntityService clientService; + + private static final Logger logger = LoggerFactory.getLogger(OAuthClientAPI.class); + + public OAuthClientAPI() { + + } + + public OAuthClientAPI(ClientDetailsEntityService clientService) { + this.clientService = clientService; + } + + // TODO: i think this needs a fancier binding than just strings on the way in + @PreAuthorize("hasRole('ROLE_ADMIN')") + @RequestMapping("/add") + public ModelAndView apiAddClient(ModelAndView modelAndView, + @RequestParam String clientId, @RequestParam String clientSecret, + @RequestParam String scope, // space delimited + @RequestParam String grantTypes, // space delimited + @RequestParam(required=false) String redirectUri, + @RequestParam String authorities, // space delimited + @RequestParam(required=false) String resourceIds, // space delimited + @RequestParam(required=false) String name, + @RequestParam(required=false) String description, + @RequestParam(required=false, defaultValue="false") boolean allowRefresh, + @RequestParam(required=false) Long accessTokenTimeout, + @RequestParam(required=false) Long refreshTokenTimeout, + @RequestParam(required=false) String owner + ) { + logger.info("apiAddClient - start"); + ClientDetailsEntity oldClient = clientService.loadClientByClientId(clientId); + if (oldClient != null) { + throw new DuplicateClientIdException(clientId); + } + + Splitter spaceDelimited = Splitter.on(" "); + // parse all of our space-delimited lists + Set scopeSet = Sets.newHashSet(spaceDelimited.split(scope)); + Set grantTypesSet = Sets.newHashSet(spaceDelimited.split(grantTypes)); // TODO: make a stronger binding to GrantTypes + logger.info("apiAddClient - before creating authorities list"); + Set authoritiesSet = Sets.newHashSet( + Iterables.transform(spaceDelimited.split(authorities), new Function() { + @Override + public GrantedAuthority apply(String auth) { + return new GrantedAuthorityImpl(auth); + } + })); + logger.info("apiAddClient - printing client details"); + logger.info("Making call to create client with " + clientId + ", " + clientSecret + + ", " + scopeSet + ", " + grantTypesSet + ", " + redirectUri + ", " + + authoritiesSet + ", " + name + ", " + description + ", " + allowRefresh + + ", " + accessTokenTimeout + ", " + refreshTokenTimeout + ", " + owner); + + Set resourceIdSet = Sets.newHashSet(spaceDelimited.split(resourceIds)); + + ClientDetailsEntity client = clientService.createClient(clientId, clientSecret, + scopeSet, grantTypesSet, redirectUri, authoritiesSet, resourceIdSet, name, description, + allowRefresh, accessTokenTimeout, refreshTokenTimeout, owner); + logger.info("apiAddClient - adding model objects"); + modelAndView.addObject("entity", client); + modelAndView.setViewName("jsonOAuthClientView"); + logger.info("apiAddClient - end"); + return modelAndView; + } + + @PreAuthorize("hasRole('ROLE_ADMIN')") + @RequestMapping("/delete") + public ModelAndView apiDeleteClient(ModelAndView modelAndView, + @RequestParam String clientId) { + + ClientDetailsEntity client = clientService.loadClientByClientId(clientId); + + if (client == null) { + throw new ClientNotFoundException("Client not found: " + clientId); + } + + clientService.deleteClient(client); + + modelAndView.setViewName("management/successfullyRemoved"); + return modelAndView; + } + + // TODO: the serializtion of this falls over, don't know why + @PreAuthorize("hasRole('ROLE_ADMIN')") + @RequestMapping("/getAll") + public ModelAndView apiGetAllClients(ModelAndView modelAndView) { + + Collection clients = clientService.getAllClients(); + modelAndView.addObject("entity", clients); + modelAndView.setViewName("jsonOAuthClientView"); + + return modelAndView; + } + + @PreAuthorize("hasRole('ROLE_ADMIN')") + @RequestMapping("/update") + public ModelAndView apiUpdateClient(ModelAndView modelAndView, + @RequestParam String clientId, @RequestParam String clientSecret, + @RequestParam String scope, // space delimited + @RequestParam String grantTypes, // space delimited + @RequestParam(required=false) String redirectUri, + @RequestParam String authorities, // space delimited + @RequestParam(required=false) String resourceIds, // space delimited + @RequestParam(required=false) String name, + @RequestParam(required=false) String description, + @RequestParam(required=false, defaultValue="false") boolean allowRefresh, + @RequestParam(required=false) Long accessTokenTimeout, + @RequestParam(required=false) Long refreshTokenTimeout, + @RequestParam(required=false) String owner + ) { + ClientDetailsEntity client = clientService.loadClientByClientId(clientId); + + if (client == null) { + throw new ClientNotFoundException("Client not found: " + clientId); + } + + Splitter spaceDelimited = Splitter.on(" "); + // parse all of our space-delimited lists + Set scopeSet = Sets.newHashSet(spaceDelimited.split(scope)); + Set grantTypesSet = Sets.newHashSet(spaceDelimited.split(grantTypes)); // TODO: make a stronger binding to GrantTypes + Set authoritiesSet = Sets.newHashSet( + Iterables.transform(spaceDelimited.split(authorities), new Function() { + @Override + public GrantedAuthority apply(String auth) { + return new GrantedAuthorityImpl(auth); + } + })); + Set resourceIdSet = Sets.newHashSet(spaceDelimited.split(resourceIds)); + + + client.setClientSecret(clientSecret); + client.setScope(scopeSet); + client.setAuthorizedGrantTypes(grantTypesSet); + client.setRegisteredRedirectUri(redirectUri); + client.setAuthorities(authoritiesSet); + client.setResourceIds(resourceIdSet); + client.setClientName(name); + client.setClientDescription(description); + client.setAllowRefresh(allowRefresh); + client.setAccessTokenTimeout(accessTokenTimeout); + client.setRefreshTokenTimeout(refreshTokenTimeout); + client.setOwner(owner); + + clientService.updateClient(client, client); + + modelAndView.addObject("entity", client); + modelAndView.setViewName("jsonOAuthClientView"); + + return modelAndView; + } + + @PreAuthorize("hasRole('ROLE_ADMIN')") + @RequestMapping("/getById") + public ModelAndView getClientById(ModelAndView modelAndView, + @RequestParam String clientId) { + + ClientDetailsEntity client = clientService.loadClientByClientId(clientId); + + if (client == null) { + throw new ClientNotFoundException("Client not found: " + clientId); + } + + modelAndView.addObject("entity", client); + modelAndView.setViewName("jsonOAuthClientView"); + + return modelAndView; + } + +} diff --git a/server/src/main/java/org/mitre/oauth2/web/OAuthClientController.java b/server/src/main/java/org/mitre/oauth2/web/OAuthClientController.java new file mode 100644 index 000000000..f96f67531 --- /dev/null +++ b/server/src/main/java/org/mitre/oauth2/web/OAuthClientController.java @@ -0,0 +1,166 @@ +/** + * + */ +package org.mitre.oauth2.web; + +import java.util.Collection; +import java.util.List; +import java.util.Set; + +import org.mitre.oauth2.model.ClientDetailsEntity; +import org.mitre.oauth2.model.OAuth2AccessTokenEntity; +import org.mitre.oauth2.model.OAuth2RefreshTokenEntity; +import org.mitre.oauth2.service.ClientDetailsEntityService; +import org.mitre.oauth2.service.OAuth2TokenEntityService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.GrantedAuthorityImpl; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.provider.AuthorizationRequest; +import org.springframework.security.oauth2.provider.OAuth2Authentication; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.servlet.ModelAndView; + +import com.google.common.collect.Sets; + + +/** + * + * Endpoint for managing OAuth2 clients + * + * @author jricher + * + */ +@Controller +@RequestMapping("/manager/oauth/clients") +public class OAuthClientController { + + private final static Set GRANT_TYPES = Sets.newHashSet("authorization_code", "client_credentials", "password", "implicit"); + + @Autowired + private ClientDetailsEntityService clientService; + + @Autowired + private OAuth2TokenEntityService tokenService; + + private Logger logger; + + public OAuthClientController() { + logger = LoggerFactory.getLogger(this.getClass()); + } + + public OAuthClientController(ClientDetailsEntityService clientService, OAuth2TokenEntityService tokenService) { + this.clientService = clientService; + this.tokenService = tokenService; + logger = LoggerFactory.getLogger(this.getClass()); + } + + /** + * Redirect to the "/" version of the root + * @param modelAndView + * @return + */ + @RequestMapping("") + public ModelAndView redirectRoot(ModelAndView modelAndView) { + modelAndView.setViewName("redirect:/manager/oauth/clients/"); + return modelAndView; + } + + /** + * View all clients + * @param modelAndView + * @return + */ + @PreAuthorize("hasRole('ROLE_ADMIN')") + @RequestMapping("/") + public ModelAndView viewAllClients(ModelAndView modelAndView) { + Authentication auth = SecurityContextHolder.getContext().getAuthentication(); + //ClientAuthenticationToken clientAuth = (ClientAuthenticationToken) ((OAuth2Authentication) auth).getClientAuthentication(); + AuthorizationRequest clientAuth = ((OAuth2Authentication) auth).getAuthorizationRequest(); + + logger.info("Client auth = " + clientAuth); + logger.info("Granted authorities = " + clientAuth.getAuthorities().toString()); + + Collection clients = clientService.getAllClients(); + modelAndView.addObject("clients", clients); + modelAndView.setViewName("/management/oauth/clientIndex"); + + return modelAndView; + } + + @PreAuthorize("hasRole('ROLE_ADMIN')") + @RequestMapping("/add") + public ModelAndView redirectAdd(ModelAndView modelAndView) { + modelAndView.setViewName("redirect:/manager/oauth/clients/add/"); + return modelAndView; + } + + @PreAuthorize("hasRole('ROLE_ADMIN')") + @RequestMapping("/add/") + public ModelAndView addClientPage(ModelAndView modelAndView) { + + Set auth = Sets.newHashSet(); + auth.add(new GrantedAuthorityImpl("ROLE_CLIENT")); + + ClientDetailsEntity client = ClientDetailsEntity.makeBuilder() + .setScope(Sets.newHashSet("scope")) + .setAuthorities(auth) // why do we have to pull this into a separate list? + .setAuthorizedGrantTypes(Sets.newHashSet("authorization_code")) + .finish(); + modelAndView.addObject("availableGrantTypes", GRANT_TYPES); + modelAndView.addObject("client", client); + + modelAndView.setViewName("/management/oauth/editClient"); + return modelAndView; + } + + @PreAuthorize("hasRole('ROLE_ADMIN')") + @RequestMapping("/delete/{clientId}") + public ModelAndView deleteClientConfirmation(ModelAndView modelAndView, + @PathVariable String clientId) { + + ClientDetailsEntity client = clientService.loadClientByClientId(clientId); + modelAndView.addObject("client", client); + modelAndView.setViewName("/management/oauth/deleteClientConfirm"); + + return modelAndView; + } + + @PreAuthorize("hasRole('ROLE_ADMIN')") + @RequestMapping("/edit/{clientId}") + public ModelAndView editClientPage(ModelAndView modelAndView, + @PathVariable String clientId) { + + ClientDetailsEntity client = clientService.loadClientByClientId(clientId); + + modelAndView.addObject("availableGrantTypes", GRANT_TYPES); + modelAndView.addObject("client", client); + modelAndView.setViewName("/management/oauth/editClient"); + + return modelAndView; + } + + @PreAuthorize("hasRole('ROLE_ADMIN')") + @RequestMapping("/view/{clientId}") + public ModelAndView viewClientDetails(ModelAndView modelAndView, + @PathVariable String clientId) { + + ClientDetailsEntity client = clientService.loadClientByClientId(clientId); + + List accessTokens = tokenService.getAccessTokensForClient(client); + List refreshTokens = tokenService.getRefreshTokensForClient(client); + + modelAndView.addObject("client", client); + modelAndView.addObject("accessTokens", accessTokens); + modelAndView.addObject("refreshTokens", refreshTokens); + + modelAndView.setViewName("/management/oauth/viewClient"); + return modelAndView; + } +} diff --git a/server/src/main/java/org/mitre/oauth2/web/OAuthConfirmationController.java b/server/src/main/java/org/mitre/oauth2/web/OAuthConfirmationController.java new file mode 100644 index 000000000..f11b509c8 --- /dev/null +++ b/server/src/main/java/org/mitre/oauth2/web/OAuthConfirmationController.java @@ -0,0 +1,70 @@ +/** + * + */ +package org.mitre.oauth2.web; + +import org.mitre.oauth2.exception.ClientNotFoundException; +import org.mitre.oauth2.service.ClientDetailsEntityService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.oauth2.provider.AuthorizationRequest; +import org.springframework.security.oauth2.provider.ClientDetails; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.SessionAttributes; +import org.springframework.web.servlet.ModelAndView; + +/** + * @author jricher + * + */ +@Controller +@SessionAttributes(types = AuthorizationRequest.class) +public class OAuthConfirmationController { + + private ClientDetailsEntityService clientService; + + public OAuthConfirmationController() { + + } + + public OAuthConfirmationController(ClientDetailsEntityService clientService) { + this.clientService = clientService; + } + + @PreAuthorize("hasRole('ROLE_USER')") + @RequestMapping("/oauth/user/approve") + public ModelAndView confimAccess(@ModelAttribute AuthorizationRequest clientAuth, + ModelAndView modelAndView) { + + ClientDetails client = clientService.loadClientByClientId(clientAuth.getClientId()); + + if (client == null) { + throw new ClientNotFoundException("Client not found: " + clientAuth.getClientId()); + } + + modelAndView.addObject("auth_request", clientAuth); + modelAndView.addObject("client", client); + modelAndView.setViewName("oauth/approve"); + + return modelAndView; + } + + /** + * @return the clientService + */ + public ClientDetailsEntityService getClientService() { + return clientService; + } + + /** + * @param clientService the clientService to set + */ + @Autowired + public void setClientService(ClientDetailsEntityService clientService) { + this.clientService = clientService; + } + + +} diff --git a/server/src/main/java/org/mitre/oauth2/web/RevocationEndpoint.java b/server/src/main/java/org/mitre/oauth2/web/RevocationEndpoint.java new file mode 100644 index 000000000..324c94f0e --- /dev/null +++ b/server/src/main/java/org/mitre/oauth2/web/RevocationEndpoint.java @@ -0,0 +1,79 @@ +package org.mitre.oauth2.web; + +import org.mitre.oauth2.exception.PermissionDeniedException; +import org.mitre.oauth2.model.OAuth2AccessTokenEntity; +import org.mitre.oauth2.model.OAuth2RefreshTokenEntity; +import org.mitre.oauth2.service.OAuth2TokenEntityService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.common.exceptions.InvalidTokenException; +import org.springframework.security.oauth2.provider.AuthorizationRequest; +import org.springframework.security.oauth2.provider.OAuth2Authentication; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.servlet.ModelAndView; + +@Controller +public class RevocationEndpoint { + @Autowired + OAuth2TokenEntityService tokenServices; + + public RevocationEndpoint() { + + } + + public RevocationEndpoint(OAuth2TokenEntityService tokenServices) { + this.tokenServices = tokenServices; + } + + // TODO + @PreAuthorize("hasRole('ROLE_ADMIN') or hasRole('ROLE_CLIENT')") + @RequestMapping("/oauth/revoke") + public ModelAndView revoke(@RequestParam("token") String tokenValue, + ModelAndView modelAndView) { + + OAuth2RefreshTokenEntity refreshToken = tokenServices.getRefreshToken(tokenValue); + OAuth2AccessTokenEntity accessToken = tokenServices.getAccessToken(tokenValue); + + if (refreshToken == null && accessToken == null) { + // TODO: this should throw a 400 with a JSON error code + throw new InvalidTokenException("Invalid OAuth token: " + tokenValue); + } + + // TODO: there should be a way to do this in SPEL, right? + Authentication auth = SecurityContextHolder.getContext().getAuthentication(); + if (auth instanceof OAuth2Authentication) { + // we've got a client acting on its own behalf, not an admin + //ClientAuthentication clientAuth = (ClientAuthenticationToken) ((OAuth2Authentication) auth).getClientAuthentication(); + AuthorizationRequest clientAuth = ((OAuth2Authentication) auth).getAuthorizationRequest(); + + if (refreshToken != null) { + if (!refreshToken.getClient().getClientId().equals(clientAuth.getClientId())) { + // trying to revoke a token we don't own, fail + // TODO: this should throw a 403 + throw new PermissionDeniedException("Client tried to revoke a token it doesn't own"); + } + } else { + if (!accessToken.getClient().getClientId().equals(clientAuth.getClientId())) { + // trying to revoke a token we don't own, fail + // TODO: this should throw a 403 + throw new PermissionDeniedException("Client tried to revoke a token it doesn't own"); + } + } + } + + // if we got this far, we're allowed to do this + if (refreshToken != null) { + tokenServices.revokeRefreshToken(refreshToken); + } else { + tokenServices.revokeAccessToken(accessToken); + } + + // TODO: throw a 200 back (no content?) + return modelAndView; + } + +} diff --git a/server/src/main/java/org/mitre/openid/connect/model/IdToken.java b/server/src/main/java/org/mitre/openid/connect/model/IdTokenClaims.java similarity index 54% rename from server/src/main/java/org/mitre/openid/connect/model/IdToken.java rename to server/src/main/java/org/mitre/openid/connect/model/IdTokenClaims.java index a7df4a097..23c2f2e57 100644 --- a/server/src/main/java/org/mitre/openid/connect/model/IdToken.java +++ b/server/src/main/java/org/mitre/openid/connect/model/IdTokenClaims.java @@ -1,5 +1,7 @@ package org.mitre.openid.connect.model; +import java.util.Date; + import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; @@ -12,10 +14,10 @@ import org.mitre.jwt.model.JwtClaims; * TODO: This class needs to be encoded as a JWT */ @Entity -public class IdToken extends JwtClaims { +public class IdTokenClaims extends JwtClaims { public static final String USER_ID = "user_id"; - public static final String ISO29115 = "iso29115"; + public static final String AUTHENTICATION_CONTEXT_CLASS_REFERENCE = "acr"; public static final String NONCE = "nonce"; public static final String AUTH_TIME = "auth_time"; @@ -46,6 +48,30 @@ public class IdToken extends JwtClaims { } - // TODO: add in other fields + public String getAuthContext() { + return getClaimAsString(AUTHENTICATION_CONTEXT_CLASS_REFERENCE); + } + + public void setAuthContext(String acr) { + setClaim(AUTHENTICATION_CONTEXT_CLASS_REFERENCE, acr); + } + + + public String getNonce() { + return getClaimAsString(NONCE); + } + + public void setNonce(String nonce) { + setClaim(NONCE, nonce); + } + + + public Date getAuthTime() { + return getClaimAsDate(AUTH_TIME); + } + + public void setAuthTime(Date authTime) { + setClaim(AUTH_TIME, authTime); + } } diff --git a/server/src/main/java/org/mitre/openid/connect/repository/IdTokenRepository.java b/server/src/main/java/org/mitre/openid/connect/repository/IdTokenRepository.java index 98676bb66..e67e7c597 100644 --- a/server/src/main/java/org/mitre/openid/connect/repository/IdTokenRepository.java +++ b/server/src/main/java/org/mitre/openid/connect/repository/IdTokenRepository.java @@ -1,11 +1,11 @@ package org.mitre.openid.connect.repository; -import org.mitre.openid.connect.model.IdToken; +import org.mitre.openid.connect.model.IdTokenClaims; public interface IdTokenRepository { - public IdToken getById(Long id); + public IdTokenClaims getById(Long id); - public IdToken save(IdToken idToken); + public IdTokenClaims save(IdTokenClaims idToken); } diff --git a/server/src/main/java/org/mitre/openid/connect/web/CheckIDEndpoint.java b/server/src/main/java/org/mitre/openid/connect/web/CheckIDEndpoint.java index 935509d85..1430acef9 100644 --- a/server/src/main/java/org/mitre/openid/connect/web/CheckIDEndpoint.java +++ b/server/src/main/java/org/mitre/openid/connect/web/CheckIDEndpoint.java @@ -1,6 +1,6 @@ package org.mitre.openid.connect.web; -import org.mitre.openid.connect.model.IdToken; +import org.mitre.openid.connect.model.IdTokenClaims; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; @@ -13,7 +13,7 @@ public class CheckIDEndpoint { @RequestMapping("/") public ModelAndView checkID(@RequestParam("id_token") String idToken, ModelAndView mav) { - IdToken token = new IdToken(); + IdTokenClaims token = new IdTokenClaims(); //TODO: Set claims diff --git a/server/src/main/java/org/mitre/pushee/openid/service/impl/OpenIDUserDetailsService.java b/server/src/main/java/org/mitre/pushee/openid/service/impl/OpenIDUserDetailsService.java new file mode 100644 index 000000000..4d608930e --- /dev/null +++ b/server/src/main/java/org/mitre/pushee/openid/service/impl/OpenIDUserDetailsService.java @@ -0,0 +1,98 @@ +package org.mitre.pushee.openid.service.impl; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.dao.DataAccessException; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.GrantedAuthorityImpl; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; + +import com.google.common.base.Splitter; +import com.google.common.collect.Iterables; + +public class OpenIDUserDetailsService implements UserDetailsService { + + private String openIdRoot; + private String admins; + private List adminList = new ArrayList(); + + private GrantedAuthority roleUser = new GrantedAuthorityImpl("ROLE_USER"); + private GrantedAuthority roleAdmin = new GrantedAuthorityImpl("ROLE_ADMIN"); + + /** + * @return the roleUser + */ + public GrantedAuthority getRoleUser() { + return roleUser; + } + + /** + * @param roleUser the roleUser to set + */ + public void setRoleUser(GrantedAuthority roleUser) { + this.roleUser = roleUser; + } + + /** + * @return the roleAdmin + */ + public GrantedAuthority getRoleAdmin() { + return roleAdmin; + } + + /** + * @param roleAdmin the roleAdmin to set + */ + public void setRoleAdmin(GrantedAuthority roleAdmin) { + this.roleAdmin = roleAdmin; + } + + public String getOpenIdRoot() { + return openIdRoot; + } + + public void setOpenIdRoot(String openIdRoot) { + this.openIdRoot = openIdRoot; + } + + public String getAdmins() { + return admins; + } + + public void setAdmins(String admins) { + this.admins = admins; + adminList.clear(); + Iterables.addAll(adminList, Splitter.on(',').omitEmptyStrings().split(admins)); + } + + public UserDetails loadUserByUsername(String identifier) throws UsernameNotFoundException, DataAccessException { + + if (identifier != null && identifier.startsWith(openIdRoot)) { + String username = identifier; + //String username = identifier.replace(openIdRoot, ""); // strip off the OpenID root + String password = "notused"; + boolean enabled = true; + boolean accountNonExpired = true; + boolean credentialsNonExpired = true; + boolean accountNonLocked = true; + List authorities = new ArrayList(); + authorities.add(roleUser); + + // calculate raw user id (SUI) + // TODO: make this more generic, right now only works with postfix names + String userid = identifier.replace(openIdRoot, ""); + + if (adminList.contains(userid)) { + authorities.add(roleAdmin); + } + + return new User(username, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, authorities); + } else { + throw new UsernameNotFoundException("Identifier " + identifier + " did not match OpenID root " + openIdRoot); + } + } +} diff --git a/server/src/main/java/org/mitre/util/jpa/JpaUtil.java b/server/src/main/java/org/mitre/util/jpa/JpaUtil.java new file mode 100644 index 000000000..9ce03c1b9 --- /dev/null +++ b/server/src/main/java/org/mitre/util/jpa/JpaUtil.java @@ -0,0 +1,37 @@ +package org.mitre.util.jpa; + +import org.springframework.dao.IncorrectResultSizeDataAccessException; +import org.springframework.transaction.annotation.Transactional; + +import javax.persistence.EntityManager; +import java.util.List; + +/** + * @author mfranklin + * Date: 4/28/11 + * Time: 2:13 PM + */ +public class JpaUtil { + public static T getSingleResult(List list) { + switch(list.size()) { + case 0: + return null; + case 1: + return list.get(0); + default: + throw new IncorrectResultSizeDataAccessException(1); + } + } + + public static T saveOrUpdate(I id, EntityManager entityManager, T entity) { + if (id == null) { + entityManager.persist(entity); + entityManager.flush(); + return entity; + } else { + T tmp = entityManager.merge(entity); + entityManager.flush(); + return tmp; + } + } +} diff --git a/spring-security-oauth b/spring-security-oauth index 7a55ca546..0d0169559 160000 --- a/spring-security-oauth +++ b/spring-security-oauth @@ -1 +1 @@ -Subproject commit 7a55ca546a3567a8a1833340d0ff9e2b47cf4bdc +Subproject commit 0d016955915e385a113e16b15d8db83a5aba4833 diff --git a/upstream-patch/secoauth_183.patch b/upstream-patch/secoauth_183.patch new file mode 100644 index 000000000..43e148be1 --- /dev/null +++ b/upstream-patch/secoauth_183.patch @@ -0,0 +1,113 @@ +diff --git a/spring-security-oauth2/src/main/java/org/springframework/security/oauth2/common/ExpiringOAuth2RefreshToken.java b/spring-security-oauth2/src/main/java/org/springframework/security/oauth2/common/ExpiringOAuth2RefreshToken.java +index 20d2512..a773b29 100644 +--- a/spring-security-oauth2/src/main/java/org/springframework/security/oauth2/common/ExpiringOAuth2RefreshToken.java ++++ b/spring-security-oauth2/src/main/java/org/springframework/security/oauth2/common/ExpiringOAuth2RefreshToken.java +@@ -9,9 +9,17 @@ public class ExpiringOAuth2RefreshToken extends OAuth2RefreshToken { + + private static final long serialVersionUID = 3449554332764129719L; + +- private final Date expiration; ++ private Date expiration; + + /** ++ * Create an expiring refresh token with a null value and no expiration ++ * @param expiration ++ */ ++ public ExpiringOAuth2RefreshToken() { ++ this(null, null); ++ } ++ ++ /** + * @param value + */ + public ExpiringOAuth2RefreshToken(String value, Date expiration) { +@@ -28,4 +36,11 @@ public class ExpiringOAuth2RefreshToken extends OAuth2RefreshToken { + return expiration; + } + ++ /** ++ * Set the expiration of this token ++ */ ++ public void setExpiration(Date expiration) { ++ this.expiration = expiration; ++ } ++ + } +diff --git a/spring-security-oauth2/src/main/java/org/springframework/security/oauth2/common/OAuth2AccessToken.java b/spring-security-oauth2/src/main/java/org/springframework/security/oauth2/common/OAuth2AccessToken.java +index 791780f..25edf77 100644 +--- a/spring-security-oauth2/src/main/java/org/springframework/security/oauth2/common/OAuth2AccessToken.java ++++ b/spring-security-oauth2/src/main/java/org/springframework/security/oauth2/common/OAuth2AccessToken.java +@@ -57,7 +57,7 @@ public class OAuth2AccessToken implements Serializable { + */ + public static String SCOPE = "scope"; + +- private final String value; ++ private String value; + + private Date expiration; + +@@ -74,8 +74,10 @@ public class OAuth2AccessToken implements Serializable { + this.value = value; + } + +- @SuppressWarnings("unused") +- private OAuth2AccessToken() { ++ /** ++ * Create an access token with no value ++ */ ++ public OAuth2AccessToken() { + this(null); + } + +@@ -88,6 +90,14 @@ public class OAuth2AccessToken implements Serializable { + return value; + } + ++ /** ++ * Set the value of the token. ++ * @param value the token value ++ */ ++ public void setValue(String value) { ++ this.value = value; ++ } ++ + public int getExpiresIn() { + return expiration != null ? Long.valueOf((expiration.getTime() - System.currentTimeMillis()) / 1000L) + .intValue() : 0; +diff --git a/spring-security-oauth2/src/main/java/org/springframework/security/oauth2/common/OAuth2RefreshToken.java b/spring-security-oauth2/src/main/java/org/springframework/security/oauth2/common/OAuth2RefreshToken.java +index 00b002c..96f3f1b 100644 +--- a/spring-security-oauth2/src/main/java/org/springframework/security/oauth2/common/OAuth2RefreshToken.java ++++ b/spring-security-oauth2/src/main/java/org/springframework/security/oauth2/common/OAuth2RefreshToken.java +@@ -15,9 +15,16 @@ public class OAuth2RefreshToken implements Serializable { + + private static final long serialVersionUID = 8349970621900575838L; + +- private final String value; ++ private String value; + + /** ++ * Create an empty token with no value ++ */ ++ public OAuth2RefreshToken() { ++ this(null); ++ } ++ ++ /** + * Create a new refresh token. + */ + @JsonCreator +@@ -35,6 +42,14 @@ public class OAuth2RefreshToken implements Serializable { + return value; + } + ++ /** ++ * Set the value of the token ++ * @param value the value of the token ++ */ ++ public void setValue(String value) { ++ this.value = value; ++ } ++ + @Override + public String toString() { + return getValue(); From 1b4ca2d70c71fa9a07fa95a324a90377121d30e4 Mon Sep 17 00:00:00 2001 From: Justin Richer Date: Fri, 6 Jan 2012 16:29:25 -0500 Subject: [PATCH 3/4] added explanation for upstream-patches directory and content --- spring-security-oauth | 2 +- upstream-patch/README.txt | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 upstream-patch/README.txt diff --git a/spring-security-oauth b/spring-security-oauth index 0d0169559..0aa98f094 160000 --- a/spring-security-oauth +++ b/spring-security-oauth @@ -1 +1 @@ -Subproject commit 0d016955915e385a113e16b15d8db83a5aba4833 +Subproject commit 0aa98f09467f90b36ed06ba5cdbb82722f08de26 diff --git a/upstream-patch/README.txt b/upstream-patch/README.txt new file mode 100644 index 000000000..cf7d5d9e5 --- /dev/null +++ b/upstream-patch/README.txt @@ -0,0 +1 @@ +This directory contains .patch files which must be applied to the upstream spring-security-oauth2 module in order for the project to build. From ed621b1b99811acc06817273591eae64fc7bacef Mon Sep 17 00:00:00 2001 From: Justin Richer Date: Fri, 6 Jan 2012 16:49:22 -0500 Subject: [PATCH 4/4] added maven default profile --- pom.xml | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/pom.xml b/pom.xml index c8f498bed..967761740 100644 --- a/pom.xml +++ b/pom.xml @@ -7,10 +7,20 @@ OpenIdConnect pom 0.1 - - spring-security-oauth/spring-security-oauth2 - server - + + + + + true + + default + + spring-security-oauth/spring-security-oauth2 + server + + + + 1.6 3.1.0.RELEASE