diff --git a/CHANGELOG.md b/CHANGELOG.md index cfc760aa..5fe5efeb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,15 @@ We follow the CalVer (https://calver.org/) versioning scheme: YY.MINOR.MICRO. +19.2.0 (2019-10-16) +=================== + +- Update the column name for OSF TOTP / 2FA model: `deleted` -> `is_deleted` +- Refactor JavaDoc, comments and code style for the OAuth module +- Refactor the main readme and add several new guides +- Fixed ORCiD login for local development +- Enable TODO comments + 19.1.2 (2019-10-07) =================== diff --git a/README.md b/README.md index 6e8e87f8..f8001bd5 100644 --- a/README.md +++ b/README.md @@ -1,394 +1,77 @@ -# Center for Open Science CAS Overlay +# Center for Open Science - CAS Overlay `Master` Build Status: [![Build Status](https://travis-ci.org/CenterForOpenScience/cas-overlay.svg?branch=master)](https://travis-ci.org/CenterForOpenScience/cas-overlay) `Develop` Build Status: [![Build Status](https://travis-ci.org/CenterForOpenScience/cas-overlay.svg?branch=develop)](https://travis-ci.org/CenterForOpenScience/cas-overlay) -Official Docs can be found [here](https://jasig.github.io/cas/) +Versioning Scheme: [![CalVer Scheme](https://img.shields.io/badge/calver-YY.MINOR.MICRO-22bfda.svg)](http://calver.org) -[CAS 4.1 Roadmap](https://wiki.jasig.org/display/CAS/CAS+4.1+Roadmap) +## About -[Docker Server](https://github.com/CenterForOpenScience/docker-library/tree/master/cas) +"Center for Open Science - CAS Overlay" is often referred to as **CAS** or **OSF CAS**. It is the centralized authentication and authorization system for [the OSF](https://osf.io/) and its services such as [Preprints](https://osf.io/preprints/), [Registries](https://osf.io/registries) and [SHARE](https://share.osf.io/). -## Configuration +### Features -### JPA Ticket Registry +* OSF Username and Password Login +* OSF Username and Verification Key Login +* OSF Two-Factor Authentication +* OSF Authentication Delegation + * [ORCiD Login with OAuth](https://github.com/CenterForOpenScience/cas-overlay/blob/develop/docs/osf-cas-as-an-oauth-client.md) + * [Institution Login with CAS](https://github.com/CenterForOpenScience/cas-overlay/blob/develop/docs/osf-cas-as-a-cas-client.md) + * [Institution Login with SAML](https://github.com/CenterForOpenScience/cas-overlay/blob/develop/docs/osf-cas-as-a-saml-sp.md) +* [OSF OAuth Provider](https://github.com/CenterForOpenScience/cas-overlay/blob/develop/docs/osf-cas-as-an-oauth-server.md) +* Login Request Throttling -* Postgres -* Apache DBCP2 (Database Connection Pooling v2) +### References -### Custom Application Authentication +The implementation of OSF CAS is based on [Yale/Jasig/Apereo CAS 4.1.x](https://github.com/apereo/cas/tree/4.1.x) using [CAS Overlay Template 4.1.x](https://github.com/apereo/cas-overlay-template/tree/4.1). Official docs from [Apereo CAS](https://www.apereo.org/projects/cas) can be found [here](https://apereo.github.io/cas/4.1.x). Learn more about the CAS protocol [here](https://apereo.github.io/cas/4.1.x/protocol/CAS-Protocol.html) or refer to [the full specification](https://apereo.github.io/cas/4.1.x/protocol/CAS-Protocol-Specification.html). -* Multi-Factor Authentication - * Time-based One Time Passwords (TOTP), e.g. Google Authenticator -* MongoDB authentication backend -* Customized login web flow prompts - * Login, Logout, One Time Password, Verification Key - * OAuth Application Approval -* [Login from external form](https://wiki.jasig.org/display/CAS/Using+CAS+from+external+link+or+custom+external+form) +## Running OSF CAS for Development -### OAuth2 Provider +### Java 8 -[Roadmap 4.1 OAuth Server Support](https://wiki.jasig.org/display/CAS/CAS+4.1+Roadmap#CAS4.1Roadmap-Oauthserversupport) +* Install Java Development Kit 8 (JDK 1.8) either from [Oracle](https://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html) or [OpenJDK](https://openjdk.java.net/install/). For macOS, it is recommended to use *THE* package manager [Homebrew](https://brew.sh/) with command `brew cask install adoptopenjdk8`. -* Token Registry - * JPA -* Scope Managers - * Simple OAuth Scope Handler - * Open Science Framework Scope Handler (MongoDB) -* Tokens - * Client Side & Web Application Server Response Types - * Authorization Code - * Token - * Refresh Token Support - * Revoke Access Tokens & Refresh Tokens - * Personal Access Tokens (Optional) - * CAS Login Access Tokens (Optional) -* Service Specific Attribute Release -* Delegated Ticket Expiration - * Access Token: 60 minutes - * Refresh Token: never-expire -* Application Integration & Maintenance Actions - * User Actions - * List Authorized Applications - * Revoke Application Tokens - * User Owned Applications & Stats - * Active User Count - * Revoke All Tokens +### JCE with Unlimited Strength -#### Profile +* Download and install [Unlimited Strength Jurisdiction Policy Files](https://www.oracle.com/java/technologies/jce-all-downloads.html) for Java Cryptography Extension (JCE). [Here](https://www.oracle.com/java/technologies/jce8-downloads.html) is the version for JDK 1.8. Unpack the ZIP file and follow the `README.txt` in the folder to back up existing cryptography policy files and install the new stronger ones. -Provides the user's principal id, any released attributes and a list of granted scopes. +### Apache Maven -GET: /oauth2/profile +* See [here](https://maven.apache.org/install.html) for how to install `maven` and [here](https://maven.apache.org/ide.html) for IDE integrations. -###### Request +### A Working OSF -``` -https://accounts.osf.io/oauth2/profile +* CAS requires a working OSF (more specifically, its database server) running locally. See [Running the OSF For Development](https://github.com/CenterForOpenScience/osf.io/blob/develop/README-docker-compose.md) for how to run OSF locally with `docker-compose`. -Authorization: Bearer AT-1-... -``` +### Database -###### Response +* CAS requires [Postgres](https://www.postgresql.org/docs/9.6/index.html) as its backend database. Use a port other than `5432` since this default one has already been taken by OSF. Update `database.url`, `database.user` and `database.password` in the [`cas.properties`](https://github.com/CenterForOpenScience/cas-overlay/blob/develop/etc/cas.properties#L141). -```json -{ - "id": "unique-user-identifier", - "scope": ["user.email", "user.profile"] -} -``` +* CAS also requires read-only access to OSF's database. No extra Postgres setup or CAS configuration is needed when running OSF locally with `docker-compose` as mentioned above. The [default](https://github.com/CenterForOpenScience/cas-overlay/blob/develop/etc/cas.properties#L94) one works as it is. -#### Web Server Authorization +### Run CAS -Secure server authorization of scopes, will need to follow up with the Authorization Code exchange. +* Refer to the [`Dockerfile`](https://github.com/CenterForOpenScience/cas-overlay/blob/develop/Dockerfile) in the repository for how to run CAS with the [Jetty Maven Plugin](https://www.eclipse.org/jetty/documentation/current/jetty-maven-plugin.html). Only the `app` and `dev` stages are relevant in this case since the `dist` one is used for production and staging servers. Take a look at the [`.travis.yml`](https://github.com/CenterForOpenScience/cas-overlay/blob/develop/.travis.yml) on how to run unit tests. You can skip `package` and go for `clean` and `install` directly; in addition, toggle the profile `nocheck` to turn unit tests on and off. -GET: /oauth2/authorize +* TL;DR, here are the commands that you need: -###### Request + ```bash + # clean and install w/ test + mvn clean install -P !nocheck + # clean and install w/o test + mvn clean install -P nocheck + # start jetty + mvn -pl cas-server-webapp/ jetty:run + ``` +* With default settings, CAS runs on port `8080` at IP address `192.168.168.167` locally. Change `server.name` here in [`cas.properties`](https://github.com/CenterForOpenScience/cas-overlay/blob/develop/etc/cas.properties#L117) if you want a different IP or port. -``` -https://accounts.osf.io/oauth2/authorize?client_id=gJgfkHAtz&redirect_uri=https%3A%2F%2Fmy-application%2Foauth%2Fcallback%2Fosf%2F&scope=user.profile%2Bwrite&state=FSyUOBgWiki_hyaBsa -``` +### A Few Extra Notes -Parameter | Value | Description -------------- | ------------- | ------------- -response_type | code | ... -client_id | ... | ... -redirect_uri | ... | ... -scope | ... | ... -state | ... | ... -access_type | **online** / offline | ... -approval_prompt | **auto** / force | ... +* To use the "Sign in with ORCiD" feature, create an application at [ORCiD Developer Tools](https://orcid.org/developer-tools) with **Redirect URI** set as `http://192.168.168.167:8080/login`. Alternatively, COS developers can use the credentials provided in https://osf.io/m2hig/wiki/home/. Update `oauth.orcid.client.id` and `oauth.orcid.client.secret` accordingly here in the [`cas.properties`](https://github.com/CenterForOpenScience/cas-overlay/blob/develop/etc/cas.properties#L68). ORCiD login will not work if CAS is run on a different `server.name` without updating 1) OSF `docker-compose` settings and 2) the **Redirect URI** of the ORCiD developer application. -###### Response +* The "Sign in through institution" feature is not available for local development. It requires a Shibboleth server sitting in front of CAS handling both SAML 2.0 authentication and TLS. -``` -https://my-application/oauth/callback/osf/?code=AC-1-3BfTHEimiGXAQPerA6Zq6cvOszjXAhzHLNQnVJhv3UPifgwVpn&state=FSyUOBgWiki_hyaBsa -``` - -Parameter | Value | Description -------------- | ------------- | ------------- -code | code | ... -state | ... | ... - -#### Client Side Authorization - -GET: /oauth2/authorize - -Allows client side javascript the ability to request specified scopes for authorization and directly return an Access Token. - -###### Request - -``` -https://accounts.osf.io/oauth2/authorize?response_type=token&client_id=gJgfkHAtz&redirect_uri=https%3A%2F%2Fmy-application%2Foauth%2Fcallback%2Fosf%2F&scope=user.profile%2Bwrite&state=FSyUOBgWiki_hyaBsa -``` - -Parameter | Value | Description -------------- | ------------- | ------------- -response_type | token | ... -client_id | ... | ... -redirect_uri | ... | ... -scope | ... | ... -state | ... | ... -approval_prompt | **auto** / force | ... - -###### Response - -``` -https://my-application/oauth/callback/osf/#access_token=AT-1-E9wpSxcUatFazdGtFFVO21i4exU9RypHbhcacgoktZ7TPUGGVf3KDuMq2RxGzKXZ6FO6if&expires_in=3600&token_type=Bearer&state=FSyUOBgWiki_hyaBsa -``` - -Parameter | Value | Description -------------- | ------------- | ------------- -access_token | ... | ... -expires_in | ... | ... -token_type | Bearer | ... -state | ... | ... - - -#### Authorization Code Exchange - -Exchange of an Authorization Code for an Access Token and potentially a Refresh Token if **offline** mode was specified. - -POST: /oauth2/token - -###### Request - -``` -https://accounts.osf.io/oauth2/token -``` - -Parameter | Value | Description -------------- | ------------- | ------------- -code | ... | ... -client_id | ... | ... -client_secret | ... | ... -redirect_uri | ... | ... -grant_type | authorization_code | ... - -###### Response - -```json -{ - "token_type": "Bearer", - "expires_in": 3600, - "refresh_token":"RT-1-SjLa4ReI4KxcxKzEj1TtIWMTEwcMY26pSy6SftrObikpsbtInb", - "access_token":"AT-1-adg7yMBUbyO4zSPVqFj2HZzOsTqNtJ5ebgk25y5UbTt4HV5W1EQ45b6PvpDtEABsaXXFBS" -} -``` - -Parameter | Value | Description -------------- | ------------- | ------------- -token_type | Bearer | ... -expires_in | ... | ... -refresh_token | ... | Included only when the authorization request was made with access_type **offline**. -access_token | ... | ... - -#### Access Token Refresh - -An authorized **offline** application may obtain a new Access Token from this endpoint. - -POST: /oauth2/token - -###### Request - -``` -https://accounts.osf.io/oauth2/token -``` - -Parameter | Value | Description -------------- | ------------- | ------------- -refresh_token | ... | ... -client_id | ... | ... -client_secret | ... | ... -grant_type | refresh_token | ... - -###### Response - -```json -{ - "token_type": "Bearer", - "expires_in": 3600, - "access_token":"AT-2-adg7yMBUbyO4zSPVqFj2HZzOsTqNtJ5ebgk25y5UbTt4HV5W1EQ45b6PvpDtEABsaXXFBS" -} -``` - -Parameter | Value | Description -------------- | ------------- | ------------- -token_type | Bearer | ... -expires_in | ... | ... -access_token | ... | ... - -#### Revoke a Token - -Handles revocation of Refresh and Access Tokens. - -POST: /oauth2/revoke - -###### Request - -``` -https://accounts.osf.io/oauth2/revoke -``` - -Parameter | Value | Description -------------- | ------------- | ------------- -token | ... | ... - -###### Response - -``` -HTTP 204 NO CONTENT -``` - -#### Revoke All Tokens Issued to a Principal - -*e.g. user revokes application access* - -Revocation of all Tokens for a specified Client ID and the given token's Principal ID. - -*If the Access Token is of type CAS any valid Client ID can be specified, otherwise the Access Token -may only revoke the Client ID it was generated with.* - -POST: /oauth2/revoke - -###### Request - -``` -https://accounts.osf.io/oauth2/revoke - -Authorization: Bearer AT-1-... -``` - -Parameter | Value | Description -------------- | ------------- | ------------- -client_id | ... | ... - -###### Response - -``` -HTTP 204 NO CONTENT -``` - -#### Revoke All Client Tokens - -*e.g. application administrator revokes all tokens* - -Revocation of all Tokens associated with the given Client ID. - -POST: /oauth2/revoke - -###### Request - -``` -https://accounts.osf.io/oauth2/revoke -``` - -Parameter | Value | Description -------------- | ------------- | ------------- -client_id | ... | ... -client_secret | ... | ... - -###### Response - -``` -HTTP 204 NO CONTENT -``` - -#### Principal Metadata - -*e.g. list applications authorized to access the user's account* - -Gathers metadata regarding token's associated with the Principal ID specified. - -*The Access Token must be type CAS.* - -POST: /oauth2/metadata - -###### Request - -``` -https://accounts.osf.io/oauth2/metadata - -Authorization: Bearer AT-1-... -``` - -###### Response - -```json -[ - { - "id": "gJgfkHAtz", - "name": "Application #1", - "description": "An simple oauth application", - "scope": [ - "user.email", - "profile.basic" - ] - }, - { - "id": "Joiuhwkjsl", - "name": "Third Party Application #2", - "description": "An oauth application", - "scope": [ - "nodes.create" - ] - } -] -``` - -#### Client Metadata - -*e.g. application information, user count, etc...* - -Provides metadata about the Client ID specified. - -POST: /oauth2/metadata - -###### Request - -``` -https://accounts.osf.io/oauth2/metadata -``` - -Parameter | Value | Description -------------- | ------------- | ------------- -client_id | ... | ... -client_secret | ... | ... - - -###### Response - -```json -{ - "id": "gJgfkHAtz", - "name": "Application #1", - "description": "An simple oauth application", - "users": 9001 -} -``` - -### Service Registry - -* Merging Service Registry Loader -* JSON Service Registry -* Open Science Framework Service Registry (MongoDB & OAuth) - -### Jetty 9.x Web Server - -* Startup Server Command - * `mvn -pl cas-server-webapp/ jetty:run` -* Optimized for faster builds - -If you have trouble building CAS via `mvn clean install`, you may need to install the "Java Cryptography Extension (JCE) Unlimited Strength -Jurisdiction Policy Files". Follow -[these instructions](http://bigdatazone.blogspot.com/2014/01/mac-osx-where-to-put-unlimited-jce-java.html) to unpack -the zip file, back up existing policy files, and install the new, stronger cryptography policy files. - - -### TODO - -* Request Throttling -* Jetty JPA Shared Sessions +* Installing `java8` with [Homebrew](https://brew.sh/) on macOS (i.e. `brew cask install java8`) [no longer works](https://github.com/ashishb/dotfiles/pull/14) due to [Oracle's new license for Java SE](https://www.oracle.com/downloads/licenses/javase-license1.html). [Here](https://github.com/Homebrew/homebrew-cask-versions/issues/7253) is the discussion. Instead, use the alternative [AdoptOpenJDK](https://adoptopenjdk.net/) and here is the [Tap](https://github.com/AdoptOpenJDK/homebrew-openjdk). +* We recommend using an IDE (e.g. [IntelliJ IDEA](https://www.jetbrains.com/idea/), [Eclipse IDE](https://www.eclipse.org/downloads/), etc.) for local development. diff --git a/cas-server-support-oauth/src/main/java/org/jasig/cas/services/OSFRegisteredService.java b/cas-server-support-oauth/src/main/java/org/jasig/cas/services/OSFRegisteredService.java index be73e856..56cfe3e4 100644 --- a/cas-server-support-oauth/src/main/java/org/jasig/cas/services/OSFRegisteredService.java +++ b/cas-server-support-oauth/src/main/java/org/jasig/cas/services/OSFRegisteredService.java @@ -24,9 +24,11 @@ import javax.persistence.Entity; /** - * Mutable registered service that uses String equality check for - * service matching. Matching is case insensitive. + * OSF registered service. * + * Mutable registered service that uses case-insensitive String equality check for service matching. + * + * @author Michael Haselton * @author Longze Chen * @since 4.1.5 */ @@ -34,6 +36,7 @@ @DiscriminatorValue("osf") public class OSFRegisteredService extends AbstractRegisteredService { + /** Unique id for serialization. */ private static final long serialVersionUID = 2028857446020394771L; private transient String servicePattern; diff --git a/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/CentralOAuthService.java b/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/CentralOAuthService.java index fa43612c..c4496b2a 100644 --- a/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/CentralOAuthService.java +++ b/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/CentralOAuthService.java @@ -39,7 +39,7 @@ import java.util.Set; /** - * Central OAuth Service. + * The CAS OAuth service interface. * * @author Michael Haselton * @author Longze Chen @@ -58,6 +58,10 @@ public interface CentralOAuthService { /** * Grant an authorization code for followup by the server. * + * The token type of the authorization code {@link AuthorizationCode} can be either {@link TokenType#OFFLINE} or + * {@link TokenType#ONLINE}, which determines 1) the token type of the access token to be granted and 2) whether + * a refresh token is to be generated in the followup. + * * @param type the token type * @param clientId the client id * @param ticketGrantingTicketId the associated ticket granting ticket id @@ -66,31 +70,49 @@ public interface CentralOAuthService { * @return an authorization code to be passed back to the client * @throws TicketException the ticket exception */ - AuthorizationCode grantAuthorizationCode(TokenType type, String clientId, String ticketGrantingTicketId, String callbackUrl, - Set scopes) throws TicketException; + AuthorizationCode grantAuthorizationCode( + TokenType type, + String clientId, + String ticketGrantingTicketId, + String callbackUrl, + Set scopes + ) throws TicketException; /** - * Grant an offline refresh token. + * Grant an OFFLINE (@link TokenType#OFFLINE} refresh token {@link RefreshToken}. * * @param authorizationCode the authorization code token * @param redirectUri the redirect uri - * @return a refresh token to be passed back to the client request. + * @return a refresh token to be passed back to the client request * @throws InvalidTokenException the invalid token exception */ - RefreshToken grantOfflineRefreshToken(AuthorizationCode authorizationCode, String redirectUri) throws InvalidTokenException; + RefreshToken grantOfflineRefreshToken( + AuthorizationCode authorizationCode, + String redirectUri + ) throws InvalidTokenException; /** - * Grant CAS Access Token. Generates an access token associated with a Ticket Granting Ticket given to the CAS client. + * Grant a CAS {@link TokenType#CAS} access token {@link AccessToken}. + * + * Generate a special CAS protocol access token associated with a ticket granting ticket given to the CAS client. + * This access token is generated by the primary CAS authentication flow and used by the CAS OAuth authorization + * flow. It is integrated in the CAS service validation phase and is released to OSF as one of the attributes. * * @param ticketGrantingTicket the ticket granting ticket * @param service the service - * @return an access token to be integrated in the CAS service validation attributes. + * @return an access token to be integrated in the CAS service validation attributes * @throws TicketException the ticket exception */ - AccessToken grantCASAccessToken(TicketGrantingTicket ticketGrantingTicket, Service service) throws TicketException; + AccessToken grantCASAccessToken( + TicketGrantingTicket ticketGrantingTicket, + Service service + ) throws TicketException; /** - * Grant a Personal Access Token. Generates an access token associated with the personal access token. + * Grant a PERSONAL {@link TokenType#PERSONAL} access token. + * + * Generates a special PERSONAL access token associated with a personal access token {@link PersonalAccessToken} + * constructed from {@code io.cos.cas.adaptors.postgres.models.OpenScienceFrameworkApiOauth2PersonalAccessToken}. * * @param personalAccessToken the personal access token * @return an access token tied to the personal access token @@ -99,7 +121,7 @@ AuthorizationCode grantAuthorizationCode(TokenType type, String clientId, String AccessToken grantPersonalAccessToken(PersonalAccessToken personalAccessToken) throws InvalidTokenException; /** - * Grant an Offline Access Token. + * Grant an OFFLINE {@link TokenType#OFFLINE} access token. * * @param refreshToken a refresh token * @return a new access token based on the refresh token provided @@ -108,7 +130,7 @@ AuthorizationCode grantAuthorizationCode(TokenType type, String clientId, String AccessToken grantOfflineAccessToken(RefreshToken refreshToken) throws InvalidTokenException; /** - * Grant an Online Access Token. + * Grant an ONLINE {@link TokenType#ONLINE} access token. * * @param authorizationCode the authorization code * @return a new access token based on the authorization code provided @@ -117,15 +139,15 @@ AuthorizationCode grantAuthorizationCode(TokenType type, String clientId, String AccessToken grantOnlineAccessToken(AuthorizationCode authorizationCode) throws InvalidTokenException; /** - * Revoke a Token. + * Revoke a token. * * @param token the token - * @return a Boolean status if the token was successfully revoked. + * @return a Boolean status if the token was successfully revoked */ Boolean revokeToken(Token token); /** - * Revoke all Tokens associated with the specified client id, authorized by the client secret. + * Revoke all tokens associated with the specified client id, authorized by the client secret. * * Note: This method is deprecated. Please avoid implementing it or using its implementations. * The functionality is and should be performed by the controller directly. @@ -133,14 +155,14 @@ AuthorizationCode grantAuthorizationCode(TokenType type, String clientId, String * @param clientId the client id * @param clientSecret the client secret */ - void revokeClientTokens(String clientId, String clientSecret); + void revokeClientTokens(String clientId, String clientSecret); /** - * Revoke all Tokens associated with the access token principal id and the client id specified. + * Revoke all tokens associated with the access token principal id and the client id specified. * * @param accessToken the access token * @param clientId the client id - * @return a Boolean status if the tokens were successfully revoked. + * @return a Boolean status if the tokens were successfully revoked */ Boolean revokeClientPrincipalTokens(AccessToken accessToken, String clientId); @@ -156,19 +178,19 @@ AuthorizationCode grantAuthorizationCode(TokenType type, String clientId, String /** * Get metadata about the principal requested, authorized by the access token. * - * @param accessToken the access token - * @return metadata regarding the access token and its principal id. + * @param accessToken the access token issued to the principal during CAS service validation + * @return metadata regarding the access token and its principal id * @throws InvalidTokenException the invalid token exception */ Collection getPrincipalMetadata(AccessToken accessToken) throws InvalidTokenException; /** - * Check if a refresh token exists for the client, principal and scopes specified. + * Check if an refresh token exists for the client, principal and scopes specified. * * @param clientId the client id * @param principalId the principal id * @param scopes the set of scopes - * @return a Boolean status if an active token was found. + * @return a Boolean status if an active token was found */ Boolean isRefreshToken(String clientId, String principalId, Set scopes); @@ -179,12 +201,12 @@ AuthorizationCode grantAuthorizationCode(TokenType type, String clientId, String * @param clientId the client id * @param principalId the principal id * @param scopes the set of scopes - * @return a Boolean status if an active token was found. + * @return a Boolean status if an active token was found */ Boolean isAccessToken(TokenType type, String clientId, String principalId, Set scopes); /** - * Get the token by the id specified. + * Get the token by the token id specified. * * @param tokenId the token id * @return a token @@ -196,7 +218,7 @@ AuthorizationCode grantAuthorizationCode(TokenType type, String clientId, String * Get the token by the id and clazz specified. * * @param tokenId the token id - * @param clazz The expected class of the token we wish to retrieve. + * @param clazz The expected class of the token we wish to retrieve * @param the generic token type to return that extends {@link Token} * @return a token * @throws InvalidTokenException the invalid token exception @@ -204,7 +226,7 @@ AuthorizationCode grantAuthorizationCode(TokenType type, String clientId, String T getToken(String tokenId, Class clazz) throws InvalidTokenException; /** - * Get the personal access token of the id specified. + * Get the PERSONAL access token by the token id specified. * * @param tokenId the token id * @return a personal access token diff --git a/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/CentralOAuthServiceImpl.java b/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/CentralOAuthServiceImpl.java index 0b41cb2f..7abbbdd0 100644 --- a/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/CentralOAuthServiceImpl.java +++ b/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/CentralOAuthServiceImpl.java @@ -24,11 +24,11 @@ import org.apache.commons.lang3.StringUtils; -import org.jasig.cas.CentralAuthenticationService; import org.jasig.cas.authentication.AuthenticationException; import org.jasig.cas.authentication.principal.Principal; import org.jasig.cas.authentication.principal.Service; import org.jasig.cas.authentication.principal.SimpleWebApplicationServiceImpl; +import org.jasig.cas.CentralAuthenticationService; import org.jasig.cas.services.ServicesManager; import org.jasig.cas.support.oauth.authentication.principal.OAuthCredential; import org.jasig.cas.support.oauth.metadata.ClientMetadata; @@ -60,15 +60,15 @@ import org.springframework.util.Assert; -import javax.validation.constraints.NotNull; - import java.util.Collection; import java.util.HashMap; import java.util.Map; import java.util.Set; +import javax.validation.constraints.NotNull; + /** - * Central OAuth Service implementation. + * The CAS OAuth service implementation of {@link CentralOAuthService}. * * @author Michael Haselton * @author Longze Chen @@ -79,73 +79,66 @@ public final class CentralOAuthServiceImpl implements CentralOAuthService { /** Log instance for logging events, info, warnings, errors, etc. */ private static final Logger LOGGER = LoggerFactory.getLogger(CentralOAuthService.class); - /** CentralAuthenticationService for requesting tickets as needed. */ + /** The primary CAS authentication service for requesting tickets as needed. */ @NotNull private final CentralAuthenticationService centralAuthenticationService; - /** ServicesManager for verifying service endpoints. */ + /** The service manager for accessing (retrieving) OAuth registered services. */ @NotNull private final ServicesManager servicesManager; - /** TokenRegistry for storing and retrieving tokens as needed. */ + /** The ticket registry for accessing (deleting) tickets as needed. */ @NotNull private final TicketRegistry ticketRegistry; - /** TokenRegistry for storing and retrieving tokens as needed. */ + /** The token registry for accessing (storing and retrieving) tokens as needed. */ @NotNull private final TokenRegistry tokenRegistry; - /** ScopeManager for storing and retrieving scopes as needed. */ + /** The scope manager for accessing (storing and retrieving) scopes as needed. */ @NotNull private final ScopeManager scopeManager; - /** PersonalAccessTokenManager for retrieving personal tokens. */ + /** The personal access token manager for accessing (retrieving) personal access tokens. */ @NotNull private final PersonalAccessTokenManager personalAccessTokenManager; - /** - * UniqueTicketIdGenerator to generate ids for AuthorizationCodes - * created. - */ + /** The unique id generator that generates ids for authorization codes created. */ @NotNull private final UniqueTicketIdGenerator authorizationCodeUniqueIdGenerator; - /** - * UniqueTicketIdGenerator to generate ids for RefreshTokens - * created. - */ + /** The unique id generator that generates ids for refresh tokens created. */ @NotNull private final UniqueTicketIdGenerator refreshTokenUniqueIdGenerator; - /** - * UniqueTicketIdGenerator to generate ids for AccessTokens - * created. - */ + /** The unique id generator that generates ids for access tokens created. */ @NotNull private final UniqueTicketIdGenerator accessTokenUniqueIdGenerator; /** - * Build the central oauth service implementation. + * Instantiates a new CAS OAuth service {@link CentralOAuthServiceImpl}. * - * @param centralAuthenticationService the central authentication service. - * @param servicesManager the services manager. - * @param ticketRegistry the ticket registry. - * @param tokenRegistry the token registry. - * @param authorizationCodeUniqueIdGenerator the authorization code unique id generator. - * @param refreshTokenUniqueIdGenerator the refresh token unique id generator. - * @param accessTokenUniqueIdGenerator the access token unique id generator. - * @param scopeManager the scope manager. - * @param personalAccessTokenManager the personal access token manager. + * @param centralAuthenticationService the central authentication service + * @param servicesManager the services manager + * @param ticketRegistry the ticket registry + * @param tokenRegistry the token registry + * @param authorizationCodeUniqueIdGenerator the authorization code unique id generator + * @param refreshTokenUniqueIdGenerator the refresh token unique id generator + * @param accessTokenUniqueIdGenerator the access token unique id generator + * @param scopeManager the scope manager + * @param personalAccessTokenManager the personal access token manager */ - public CentralOAuthServiceImpl(final CentralAuthenticationService centralAuthenticationService, - final ServicesManager servicesManager, - final TicketRegistry ticketRegistry, - final TokenRegistry tokenRegistry, - final UniqueTicketIdGenerator authorizationCodeUniqueIdGenerator, - final UniqueTicketIdGenerator refreshTokenUniqueIdGenerator, - final UniqueTicketIdGenerator accessTokenUniqueIdGenerator, - final ScopeManager scopeManager, - final PersonalAccessTokenManager personalAccessTokenManager) { + public CentralOAuthServiceImpl( + final CentralAuthenticationService centralAuthenticationService, + final ServicesManager servicesManager, + final TicketRegistry ticketRegistry, + final TokenRegistry tokenRegistry, + final UniqueTicketIdGenerator authorizationCodeUniqueIdGenerator, + final UniqueTicketIdGenerator refreshTokenUniqueIdGenerator, + final UniqueTicketIdGenerator accessTokenUniqueIdGenerator, + final ScopeManager scopeManager, + final PersonalAccessTokenManager personalAccessTokenManager + ) { this.centralAuthenticationService = centralAuthenticationService; this.servicesManager = servicesManager; this.ticketRegistry = ticketRegistry; @@ -163,67 +156,97 @@ public OAuthRegisteredService getRegisteredService(final String clientId) { } @Override - public AuthorizationCode grantAuthorizationCode(final TokenType type, final String clientId, - final String ticketGrantingTicketId, final String redirectUri, - final Set scopes) throws TicketException { + public AuthorizationCode grantAuthorizationCode( + final TokenType type, + final String clientId, + final String ticketGrantingTicketId, + final String redirectUri, + final Set scopes + ) throws TicketException { + final Service service = new SimpleWebApplicationServiceImpl(redirectUri); - final ServiceTicket serviceTicket = centralAuthenticationService.grantServiceTicket(ticketGrantingTicketId, service); + final ServiceTicket serviceTicket + = centralAuthenticationService.grantServiceTicket(ticketGrantingTicketId, service); final AuthorizationCodeImpl authorizationCode = new AuthorizationCodeImpl( this.authorizationCodeUniqueIdGenerator.getNewTicketId(AuthorizationCode.PREFIX), - type, clientId, serviceTicket.getGrantingTicket().getAuthentication().getPrincipal().getId(), - serviceTicket, scopes); + type, + clientId, + serviceTicket.getGrantingTicket().getAuthentication().getPrincipal().getId(), + serviceTicket, + scopes + ); + + // TODO: add token type to the logger LOGGER.debug("{} : {}", OAuthConstants.AUTHORIZATION_CODE, authorizationCode); this.tokenRegistry.addToken(authorizationCode); - return authorizationCode; } @Override - public RefreshToken grantOfflineRefreshToken(final AuthorizationCode authorizationCode, final String redirectUri) - throws InvalidTokenException { - final Principal principal = authorizationCode.getServiceTicket().getGrantingTicket().getAuthentication().getPrincipal(); - final OAuthCredential credential = new OAuthCredential(principal.getId(), principal.getAttributes(), TokenType.OFFLINE); - + public RefreshToken grantOfflineRefreshToken( + final AuthorizationCode authorizationCode, + final String redirectUri + ) throws InvalidTokenException { + + final Principal principal + = authorizationCode.getServiceTicket().getGrantingTicket().getAuthentication().getPrincipal(); + final OAuthCredential credential + = new OAuthCredential(principal.getId(), principal.getAttributes(), TokenType.OFFLINE); final TicketGrantingTicket ticketGrantingTicket; try { ticketGrantingTicket = centralAuthenticationService.createTicketGrantingTicket(credential); } catch (final AuthenticationException | TicketException e) { throw new InvalidTokenException(authorizationCode.getId()); } - final RefreshToken refreshToken = new RefreshTokenImpl( - refreshTokenUniqueIdGenerator.getNewTicketId(RefreshToken.PREFIX), authorizationCode.getClientId(), + refreshTokenUniqueIdGenerator.getNewTicketId(RefreshToken.PREFIX), + authorizationCode.getClientId(), authorizationCode.getServiceTicket().getGrantingTicket().getAuthentication().getPrincipal().getId(), - ticketGrantingTicket, authorizationCode.getServiceTicket().getService(), authorizationCode.getScopes()); - LOGGER.debug("Offline {} : {}", OAuthConstants.REFRESH_TOKEN, refreshToken); - - // remove the service ticket, doing so will cascade and remove the authorization code token + ticketGrantingTicket, + authorizationCode.getServiceTicket().getService(), + authorizationCode.getScopes() + ); + LOGGER.debug("OFFLINE {} : {}", OAuthConstants.REFRESH_TOKEN, refreshToken); + + // Remove the service ticket, doing so will CASCADE and remove the authorization code. + // For `AuthorizationCodeImpl`, both `.getTicket()` and `getServiceTicket()` returns the service ticket. ticketRegistry.deleteTicket(authorizationCode.getTicket().getId()); - tokenRegistry.addToken(refreshToken); + tokenRegistry.addToken(refreshToken); return refreshToken; } @Override - public AccessToken grantCASAccessToken(final TicketGrantingTicket ticketGrantingTicket, final Service service) - throws TicketException { + public AccessToken grantCASAccessToken( + final TicketGrantingTicket ticketGrantingTicket, + final Service service + ) throws TicketException { + final AccessToken accessToken = new AccessTokenImpl( - accessTokenUniqueIdGenerator.getNewTicketId(AccessToken.PREFIX), TokenType.CAS, null, - ticketGrantingTicket.getAuthentication().getPrincipal().getId(), ticketGrantingTicket, service, null, - scopeManager.getCASScopes()); + accessTokenUniqueIdGenerator.getNewTicketId(AccessToken.PREFIX), + TokenType.CAS, + null, + ticketGrantingTicket.getAuthentication().getPrincipal().getId(), + ticketGrantingTicket, + service, + null, + scopeManager.getCASScopes() + ); LOGGER.debug("CAS {} : {}", OAuthConstants.ACCESS_TOKEN, accessToken); tokenRegistry.addToken(accessToken); - return accessToken; } @Override - public AccessToken grantPersonalAccessToken(final PersonalAccessToken personalAccessToken) throws InvalidTokenException { - final OAuthCredential credential = new OAuthCredential(personalAccessToken.getPrincipalId(), TokenType.PERSONAL); + public AccessToken grantPersonalAccessToken( + final PersonalAccessToken personalAccessToken + ) throws InvalidTokenException { + final OAuthCredential credential + = new OAuthCredential(personalAccessToken.getPrincipalId(), TokenType.PERSONAL); final TicketGrantingTicket ticketGrantingTicket; try { ticketGrantingTicket = centralAuthenticationService.createTicketGrantingTicket(credential); @@ -232,41 +255,58 @@ public AccessToken grantPersonalAccessToken(final PersonalAccessToken personalAc } final AccessToken accessToken = new AccessTokenImpl( - personalAccessToken.getId(), TokenType.PERSONAL, null, personalAccessToken.getPrincipalId(), ticketGrantingTicket, - null, null, personalAccessToken.getScopes()); - LOGGER.debug("Personal {} : {}", OAuthConstants.ACCESS_TOKEN, accessToken); + personalAccessToken.getId(), + TokenType.PERSONAL, + null, + personalAccessToken.getPrincipalId(), + ticketGrantingTicket, + null, + null, + personalAccessToken.getScopes() + ); + LOGGER.debug("PERSONAL {} : {}", OAuthConstants.ACCESS_TOKEN, accessToken); tokenRegistry.addToken(accessToken); - return accessToken; } @Override public AccessToken grantOfflineAccessToken(final RefreshToken refreshToken) throws InvalidTokenException { + final ServiceTicket serviceTicket; try { - serviceTicket = centralAuthenticationService.grantServiceTicket(refreshToken.getTicketGrantingTicket().getId(), - refreshToken.getService()); + serviceTicket = centralAuthenticationService + .grantServiceTicket( + refreshToken.getTicketGrantingTicket().getId(), + refreshToken.getService() + ); } catch (final TicketException e) { throw new InvalidTokenException(refreshToken.getId()); } final AccessToken accessToken = new AccessTokenImpl( - accessTokenUniqueIdGenerator.getNewTicketId(AccessToken.PREFIX), TokenType.OFFLINE, refreshToken.getClientId(), - refreshToken.getTicketGrantingTicket().getAuthentication().getPrincipal().getId(), null, null, - serviceTicket, refreshToken.getScopes()); - LOGGER.debug("Offline {} : {}", OAuthConstants.ACCESS_TOKEN, accessToken); + accessTokenUniqueIdGenerator.getNewTicketId(AccessToken.PREFIX), + TokenType.OFFLINE, + refreshToken.getClientId(), + refreshToken.getTicketGrantingTicket().getAuthentication().getPrincipal().getId(), + null, + null, + serviceTicket, + refreshToken.getScopes() + ); + LOGGER.debug("OFFLINE {} : {}", OAuthConstants.ACCESS_TOKEN, accessToken); tokenRegistry.addToken(accessToken); - return accessToken; } @Override public AccessToken grantOnlineAccessToken(final AuthorizationCode authorizationCode) throws InvalidTokenException { - final Principal principal = authorizationCode.getServiceTicket().getGrantingTicket().getAuthentication().getPrincipal(); - final OAuthCredential credential = new OAuthCredential(principal.getId(), principal.getAttributes(), TokenType.ONLINE); + final Principal principal + = authorizationCode.getServiceTicket().getGrantingTicket().getAuthentication().getPrincipal(); + final OAuthCredential credential + = new OAuthCredential(principal.getId(), principal.getAttributes(), TokenType.ONLINE); final TicketGrantingTicket ticketGrantingTicket; try { ticketGrantingTicket = centralAuthenticationService.createTicketGrantingTicket(credential); @@ -275,20 +315,28 @@ public AccessToken grantOnlineAccessToken(final AuthorizationCode authorizationC } final AccessToken accessToken = new AccessTokenImpl( - accessTokenUniqueIdGenerator.getNewTicketId(AccessToken.PREFIX), TokenType.ONLINE, authorizationCode.getClientId(), + accessTokenUniqueIdGenerator.getNewTicketId(AccessToken.PREFIX), + TokenType.ONLINE, + authorizationCode.getClientId(), authorizationCode.getServiceTicket().getGrantingTicket().getAuthentication().getPrincipal().getId(), - ticketGrantingTicket, authorizationCode.getServiceTicket().getService(), null, authorizationCode.getScopes()); - LOGGER.debug("Online {} : {}", OAuthConstants.ACCESS_TOKEN, accessToken); - - // remove the service ticket, doing so will cascade and remove the authorization code token + ticketGrantingTicket, + authorizationCode.getServiceTicket().getService(), + null, + authorizationCode.getScopes() + ); + LOGGER.debug("ONLINE {} : {}", OAuthConstants.ACCESS_TOKEN, accessToken); + + // Remove the service ticket, doing so will CASCADE and remove the authorization code. + // For `AuthorizationCodeImpl`, both `.getTicket()` and `getServiceTicket()` returns the service ticket. ticketRegistry.deleteTicket(authorizationCode.getTicket().getId()); - tokenRegistry.addToken(accessToken); + tokenRegistry.addToken(accessToken); return accessToken; } @Override public Boolean revokeToken(final Token token) { + // Remove the ticket, doing so will CASCADE and remove the token. return ticketRegistry.deleteTicket(token.getTicket().getId()); } @@ -299,15 +347,18 @@ public void revokeClientTokens(final String clientId, final String clientSecret) @Override public Boolean revokeClientPrincipalTokens(final AccessToken accessToken, final String clientId) { + final String targetClientId; + // TODO: either add a check for PERSONAL access tokens or treat PERSONAL access token as CAS token if (accessToken.getType() == TokenType.CAS) { - // Only CAS Tokens are allowed to specify the client id for revocation. + // CAS access token is not bound to a client but to a principal. Must specify the client id. if (StringUtils.isBlank(clientId)) { LOGGER.warn("CAS Token used for revocation, Client ID must be specified"); return Boolean.FALSE; } targetClientId = clientId; } else { + // ONLINE and OFFLINE access tokens is bound to a client. Must provide the correct client id. if (!accessToken.getClientId().equals(clientId)) { LOGGER.warn("Access Token's Client ID and specified Client ID must match"); return Boolean.FALSE; @@ -315,17 +366,30 @@ public Boolean revokeClientPrincipalTokens(final AccessToken accessToken, final targetClientId = accessToken.getClientId(); } - final Collection refreshTokens = tokenRegistry.getClientPrincipalTokens(targetClientId, - accessToken.getPrincipalId(), RefreshToken.class); + final Collection refreshTokens = tokenRegistry + .getClientPrincipalTokens( + targetClientId, + accessToken.getPrincipalId(), + RefreshToken.class + ); for (final RefreshToken token : refreshTokens) { LOGGER.debug("Revoking refresh token : {}", token.getId()); + // Remove the ticket granting ticket, doing so will CASCADE and remove 1) the refresh token and 2) the + // service tickets created by the ticket granting ticket for generating OFFLINE access tokens which will + // further CASCADE and remove all the OFFLINE access tokens. ticketRegistry.deleteTicket(token.getTicketGrantingTicket().getId()); } - final Collection accessTokens = tokenRegistry.getClientPrincipalTokens(targetClientId, - accessToken.getPrincipalId(), TokenType.ONLINE, AccessToken.class); + final Collection accessTokens = tokenRegistry + .getClientPrincipalTokens( + targetClientId, + accessToken.getPrincipalId(), + TokenType.ONLINE, + AccessToken.class + ); for (final AccessToken token : accessTokens) { LOGGER.debug("Revoking access token : {}", token.getId()); + // Remove the ticket granting ticket, doing so will CASCADE and remove the ONLINE access token. ticketRegistry.deleteTicket(token.getTicketGrantingTicket().getId()); } @@ -334,6 +398,7 @@ public Boolean revokeClientPrincipalTokens(final AccessToken accessToken, final @Override public ClientMetadata getClientMetadata(final String clientId, final String clientSecret) { + final OAuthRegisteredService service = getRegisteredService(clientId); if (service == null) { LOGGER.error("OAuth Registered Service could not be found for clientId : {}", clientId); @@ -344,45 +409,56 @@ public ClientMetadata getClientMetadata(final String clientId, final String clie return null; } - return new ClientMetadata(service.getClientId(), service.getName(), service.getDescription(), - tokenRegistry.getPrincipalCount(clientId)); + return new ClientMetadata( + service.getClientId(), + service.getName(), + service.getDescription(), + tokenRegistry.getPrincipalCount(clientId) + ); } @Override - public Collection getPrincipalMetadata(final AccessToken accessToken) - throws InvalidTokenException { + public Collection getPrincipalMetadata( + final AccessToken accessToken + ) throws InvalidTokenException { + if (accessToken.getType() != TokenType.CAS) { - // Only CAS Tokens are allowed to access principal metadata. - LOGGER.warn("Principal Metadata can only be accessed with an Access Token of type CAS"); + // Only CAS access tokens are allowed to access principal metadata. + LOGGER.warn("Principal metadata can only be accessed with an access token of type CAS"); throw new InvalidTokenException(accessToken.getId()); } final Map metadata = new HashMap<>(); + + // TODO: Fix code duplication for (final Token token : tokenRegistry.getPrincipalTokens(accessToken.getPrincipalId(), RefreshToken.class)) { final PrincipalMetadata serviceDetail; if (!metadata.containsKey(token.getClientId())) { final OAuthRegisteredService service = getRegisteredService(token.getClientId()); - - serviceDetail = new PrincipalMetadata(service.getClientId(), service.getName(), service.getDescription()); - metadata.put(token.getClientId(), serviceDetail); + serviceDetail = new PrincipalMetadata( + service.getClientId(), + service.getName(), + service.getDescription()); + metadata.put(token.getClientId(), serviceDetail + ); } else { serviceDetail = metadata.get(token.getClientId()); } - serviceDetail.getScopes().addAll(token.getScopes()); } - for (final Token token : tokenRegistry.getPrincipalTokens(accessToken.getPrincipalId(), AccessToken.class)) { final PrincipalMetadata serviceDetail; if (!metadata.containsKey(token.getClientId())) { final OAuthRegisteredService service = getRegisteredService(token.getClientId()); - - serviceDetail = new PrincipalMetadata(service.getClientId(), service.getName(), service.getDescription()); + serviceDetail = new PrincipalMetadata( + service.getClientId(), + service.getName(), + service.getDescription() + ); metadata.put(token.getClientId(), serviceDetail); } else { serviceDetail = metadata.get(token.getClientId()); } - serviceDetail.getScopes().addAll(token.getScopes()); } @@ -395,20 +471,25 @@ public Boolean isRefreshToken(final String clientId, final String principalId, f } @Override - public Boolean isAccessToken(final TokenType type, final String clientId, final String principalId, - final Set scopes) { + public Boolean isAccessToken( + final TokenType type, + final String clientId, + final String principalId, + final Set scopes + ) { return tokenRegistry.isToken(type, clientId, principalId, scopes, AccessToken.class); } @Override public Token getToken(final String tokenId) throws InvalidTokenException { - Assert.notNull(tokenId, "tokenId cannot be null"); + Assert.notNull(tokenId, "tokenId cannot be null"); if (tokenId.startsWith(AuthorizationCode.PREFIX)) { return getToken(tokenId, AuthorizationCode.class); } else if (tokenId.startsWith(RefreshToken.PREFIX)) { return getToken(tokenId, RefreshToken.class); } + // PERSONAL access tokens do not have the `accessToken.PREFIX` that is used by both OFFLINE and ONLINE ones return getToken(tokenId, AccessToken.class); } @@ -416,8 +497,8 @@ public Token getToken(final String tokenId) throws InvalidTokenException { @Timed(name = "GET_TOKEN_TIMER") @Metered(name = "GET_TOKEN_METER") @Counted(name="GET_TOKEN_COUNTER", monotonic=true) - public T getToken(final String tokenId, final Class clazz) - throws InvalidTokenException { + public T getToken(final String tokenId, final Class clazz) throws InvalidTokenException { + Assert.notNull(tokenId, "tokenId cannot be null"); final T token = this.tokenRegistry.getToken(tokenId, clazz); @@ -425,11 +506,9 @@ public T getToken(final String tokenId, final Class clazz) LOGGER.error("Token [{}] by type [{}] cannot be found in the token registry.", tokenId, clazz.getSimpleName()); throw new InvalidTokenException(tokenId); } - if (token.getTicket().isExpired()) { - // cleanup the expired ticket and token. + // Remove the expired ticket, which will CASCADE and remove the token. ticketRegistry.deleteTicket(token.getTicket().getId()); - LOGGER.error("Token [{}] ticket [{}] is expired.", tokenId, token.getTicket().getId()); throw new InvalidTokenException(tokenId); } @@ -439,6 +518,7 @@ public T getToken(final String tokenId, final Class clazz) @Override public PersonalAccessToken getPersonalAccessToken(final String tokenId) { + Assert.notNull(tokenId, "tokenId cannot be null"); if (personalAccessTokenManager != null) { @@ -450,10 +530,11 @@ public PersonalAccessToken getPersonalAccessToken(final String tokenId) { @Override public Map getScopes(final Set scopeSet) throws InvalidScopeException { + Assert.notNull(scopeSet, "scopeSet cannot be null"); final Map scopeMap = new HashMap<>(); - + // Add the given set of scopes. for (final String scope : scopeSet) { final Scope oAuthScope = scopeManager.getScope(scope); if (oAuthScope == null) { @@ -462,7 +543,7 @@ public Map getScopes(final Set scopeSet) throws InvalidSc } scopeMap.put(oAuthScope.getName(), oAuthScope); } - + // Add the default set of scopes. for (final Scope defaultScope : scopeManager.getDefaults()) { scopeMap.put(defaultScope.getName(), defaultScope); } diff --git a/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/InvalidParameterException.java b/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/InvalidParameterException.java index eabd1abc..02e8de04 100644 --- a/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/InvalidParameterException.java +++ b/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/InvalidParameterException.java @@ -24,20 +24,24 @@ * The exception to throw when a parameter is invalid or missing. * * @author Michael Haselton - * @since 4.1.0 + * @author Longze Chen + * @since 4.1.5 */ public class InvalidParameterException extends RootCasException { + /** Log instance for logging events, info, warnings, errors, etc. */ private static final long serialVersionUID = -6122987319096575896L; /** The code description. */ private static final String CODE = "INVALID_PARAMETER"; + /** The name of the invalid or missing parameter. */ private final String name; /** - * Constructs a InvalidParameterException with the default exception code. - * @param name the name of the parameter that originally caused this exception to be thrown. + * Instantiates a {@link InvalidParameterException} with the default exception code and a given parameter name. + * + * @param name the name of the parameter that originally caused this exception to be thrown */ public InvalidParameterException(final String name) { super(CODE); @@ -45,8 +49,8 @@ public InvalidParameterException(final String name) { } /** - * Constructs a InvalidParameterException with the default exception code and - * the original exception that was thrown. + * Instantiates a {@link InvalidParameterException} with the default exception code, the original exception that + * was thrown and a given parameter name. * * @param throwable the chained exception * @param name the name of the parameter that originally caused this exception to be thrown. @@ -58,6 +62,7 @@ public InvalidParameterException(final Throwable throwable, final String name) { /** * Returns the message of this exception. + * * @return the message * @see InvalidParameterException#name */ diff --git a/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/OAuthConstants.java b/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/OAuthConstants.java index 65d5273d..bc201b00 100644 --- a/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/OAuthConstants.java +++ b/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/OAuthConstants.java @@ -19,197 +19,253 @@ package org.jasig.cas.support.oauth; /** - * This class has the main constants for the OAuth implementation. + * This class has the main constants for the OAuth 2.0 implementation. + * + * These constants are used in many places such as OAuth request URLs, redirect response URLs, HTTP headers and body, + * OAuth session data, error name and messages, etc. However, the naming of the constants is not accurate and at times + * confusing. Thus, refer to the JavaDoc comments and the actual usage for what they are and how they are used. * * @author Jerome Leleu * @author Michael Haselton - * @since 3.5.0 + * @author Longze Chen + * @since 4.1.5 */ public interface OAuthConstants { - /** The redirect uri. */ + /** The name of the HTTP authorization header. */ + String AUTHORIZATION_HEADER = "Authorization"; + + /** The name of the redirect uri parameter. */ String REDIRECT_URI = "redirect_uri"; - /** The response type. */ + /** The name of the response type parameter. */ String RESPONSE_TYPE = "response_type"; - /** The client id. */ + /** The name of the client id parameter. */ String CLIENT_ID = "client_id"; - /** The client secret. */ + /** The name of the client secret parameter. */ String CLIENT_SECRET = "client_secret"; - /** The scope. */ + /** The name of the scope parameter (OAuth protocol, as opposed to the CAS protocol one). */ String SCOPE = "scope"; - /** The approval prompt. */ + /** The name of the principal id parameter. */ + String PRINCIPAL_ATTRIBUTES = "attributes"; + + /** The name of the principal id parameter. */ + String PRINCIPAL_ID = "id"; + + /** The name of the service name parameter. */ + String SERVICE_NAME = "name"; + + /** The name of the service users parameter. */ + String SERVICE_USERS = "users"; + + /** The name of the service description parameter. */ + String SERVICE_DESCRIPTION = "description"; + + /** The name of the access type parameter. */ String ACCESS_TYPE = "access_type"; - /** The approval prompt. */ + /** The name of the approval prompt parameter. */ String APPROVAL_PROMPT = "approval_prompt"; - /** The approval prompt force. */ + /** One value for the approval prompt parameter: force. */ String APPROVAL_PROMPT_FORCE = "force"; - /** The approval prompt auto. */ + /** The other value for the approval prompt parameter: auto. */ String APPROVAL_PROMPT_AUTO = "auto"; - /** The bypass approval prompt. */ - String BYPASS_APPROVAL_PROMPT = "bypass_approval_prompt"; - - /** The code. */ + /** The name of the code parameter, of which the value stores an authorization code. */ String CODE = "code"; - /** The service. */ + /** The name of the service parameter, of which the value is a full callback authorize URL. */ String SERVICE = "service"; - /** The ticket. */ + /** The name of the service ticket parameter. */ String TICKET = "ticket"; - /** The token. */ + /** The name of the token parameter. */ String TOKEN = "token"; - /** The state. */ + /** The name of the state parameter. */ String STATE = "state"; - /** The access token. */ + /** The name of access token parameter (OAuth protocol, as opposed to the CAS protocol one). */ String ACCESS_TOKEN = "access_token"; - /** The refresh token. */ + /** The name of the refresh token parameter OR one value for the grant type parameter for refresh token. */ String REFRESH_TOKEN = "refresh_token"; - /** The grant type. */ + /** The name of the grant type parameter. */ String GRANT_TYPE = "grant_type"; - /** The authorization code. */ + /** The other value of the grant type parameter for authorization code. */ String AUTHORIZATION_CODE = "authorization_code"; - /** The bearer token. */ + /** The bearer token prefix in the authorization header OR one of the value for the token type parameter. */ String BEARER_TOKEN = "Bearer"; - /** The OATH h20_ approval prompt action. */ + /** The name of the OAuth approval prompt action parameter. */ String OAUTH20_APPROVAL_PROMPT_ACTION = "action"; - /** The OATH h20_ approval prompt allow action. */ + /** The only valid value for the OAuth approval prompt action parameter: allow. */ String OAUTH20_APPROVAL_PROMPT_ACTION_ALLOW = "allow"; - /** The OAUT h20_ redirect uri. */ + /** OAuth session parameter: redirect uri. */ String OAUTH20_REDIRECT_URI = "oauth20_redirect_uri"; - /** The OAUT h20_ login ticket id. */ + /** OAuth session parameter: login ticket id. */ String OAUTH20_LOGIN_TICKET_ID = "oauth20_login_ticket_id"; - /** The OAUT h20_ servic e_ name. */ + /** OAuth session parameter: service name. */ String OAUTH20_SERVICE_NAME = "oauth20_service_name"; - /** The OAUT h20_ state. */ + /** OAuth session parameter: state. */ String OAUTH20_STATE = "oauth20_state"; - /** The OAUT h20_ scope. */ + /** OAuth session parameter: scope. */ String OAUTH20_SCOPE = "oauth20_scope"; - /** The OAUT h20_ scope map. */ + /** OAuth session parameter: scope map. */ String OAUTH20_SCOPE_SET = "oauth20_scope_set"; - /** The OAUT h20_ response type. */ + /** OAuth session parameter: response type. */ String OAUTH20_RESPONSE_TYPE = "oauth20_response_type"; - /** The OAUT h20_ client id. */ + /** OAuth session parameter: client id. */ String OAUTH20_CLIENT_ID = "oauth20_client_id"; - /** The OAUT h20_ token type. */ + /** OAuth session parameter: token type. */ String OAUTH20_TOKEN_TYPE = "oauth20_token_type"; - /** The OAUT h20_ approval prompt. */ + /** OAuth session parameter: approval prompt. */ String OAUTH20_APPROVAL_PROMPT = "oauth20_approval_prompt"; - /** The missing access token. */ + /** OAuth session parameter: bypass approval prompt. */ + String BYPASS_APPROVAL_PROMPT = "bypass_approval_prompt"; + + /** Error name for missing access token. */ String MISSING_ACCESS_TOKEN = "missing_access_token"; - /** The expired access token. */ + /** Error name for expired access token. */ String EXPIRED_ACCESS_TOKEN = "expired_access_token"; - /** The confirm view. */ + /** Bean configuration: the name of the OAuth confirm view. */ String CONFIRM_VIEW = "oauthConfirmView"; - /** The error view. */ + /** Bean configuration: the name of the OAuth failure view. */ String ERROR_VIEW = "oauthFailureView"; - /** The invalid request. */ + /** Error name for invalid requests, e.g. invalid or missing redirect uri, client id and secret. */ String INVALID_REQUEST = "invalid_request"; - /** The unauthorized request. */ + /** Error name for invalid access token. */ String UNAUTHORIZED_REQUEST = "unauthorized"; - /** The invalid grant. */ + /** Error name for invalid grants, e.g. invalid grant type, expired ST and TGT. */ String INVALID_GRANT = "invalid_grant"; - /** The authorize url. */ + /** The OAuth authorize endpoint. */ String AUTHORIZE_URL = "authorize"; - /** The callback authorize url. */ + /** The path for OAuth callback authorize endpoint. */ String CALLBACK_AUTHORIZE_URL = "callbackAuthorize"; - /** The callback authorize url. */ + /** The path for OAuth callback authorize action endpoint. */ String CALLBACK_AUTHORIZE_ACTION_URL = "callbackAuthorizeAction"; - /** The access token url. */ + /** The path for OAuth 2.0 access token endpoint. */ String TOKEN_URL = "token"; - /** The revoke token url. */ + /** The path for OAuth 2.0 revoke token endpoint. */ String REVOKE_URL = "revoke"; - /** The profile url. */ + /** The path for OAuth 2.0 profile endpoint. */ String PROFILE_URL = "profile"; - /** The metadata url. */ + /** The path for OAuth 2.0 metadata endpoint. */ String METADATA_URL = "metadata"; - /** The remaining time in seconds before expiration with syntax : expires_in: 3600... */ + /** The name of the expiration time parameter. */ String EXPIRES_IN = "expires_in"; - /** The token type. */ + /** The name of the token type parameter. */ String TOKEN_TYPE = "token_type"; - /** The error. */ + /** The name of the error parameter in the denied callback URL. */ String ERROR = "error"; - /** The access denied. */ + /** The value for the error parameter in the denied callback URL for access denied. */ String ACCESS_DENIED = "access_denied"; - /** The CAS protocol access token. */ + /** The attribute name of the access token (CAS protocol, as opposed to the OAuth protocol one. */ String CAS_PROTOCOL_ACCESS_TOKEN = "accessToken"; - /** The CAS protocol access token scope. */ + /** The attribute name of access token scope (CAS protocol, as opposed to the OAuth protocol one. */ String CAS_PROTOCOL_ACCESS_TOKEN_SCOPE = "accessTokenScope"; - /** The invalid code error description. */ + /** Error description for invalid authorization code. */ String INVALID_CODE_DESCRIPTION = "Invalid Code"; - /** The invalid refresh token error description. */ + /** Error description for missing authorization code. */ + String MISSING_CODE_DESCRIPTION = "Missing Code"; + + /** Error description for invalid refresh token. */ String INVALID_REFRESH_TOKEN_DESCRIPTION = "Invalid Refresh Token"; - /** The invalid access token error description. */ + /** Error description for missing refresh token. */ + String MISSING_REFRESH_TOKEN_DESCRIPTION = "Missing Refresh Token"; + + /** Error description for invalid access token. */ String INVALID_ACCESS_TOKEN_DESCRIPTION = "Invalid Access Token"; - /** The missing access token error description. */ + /** Error description for invalid access token type. */ + String INVALID_ACCESS_TOKEN_TYPE_DESCRIPTION = "Invalid Access Token Type"; + + /** Error description for missing access token. */ String MISSING_ACCESS_TOKEN_DESCRIPTION = "Missing Access Token"; - /** The ticket granting ticket expired error description. */ + /** Error description for the invalid token. */ + String INVALID_TOKEN_DESCRIPTION = "Invalid Token"; + + /** Error description for the missing token. */ + String MISSING_TOKEN_DESCRIPTION = "Missing Token"; + + /** Error description for ticket granting ticket expired. */ String EXPIRED_TGT_DESCRIPTION = "Ticket Granting Ticket Expired"; - /** The service ticket expired error description. */ + /** Error description for service ticket expired. */ String EXPIRED_ST_DESCRIPTION = "Service Ticket Expired"; - /** The invalid client id or client secret error description. */ + /** Error description for missing client id. */ + String MISSING_CLIENT_ID_DESCRIPTION = "Missing Client ID"; + + /** Error description for unknown client id. */ + String UNKNOWN_CLIENT_ID_DESCRIPTION = "Unknown Client ID"; + + /** Error description for missing client secret. */ + String MISSING_CLIENT_SECRET_DESCRIPTION = "Missing Client Secret"; + + /** Error description for invalid client secret. */ + String INVALID_CLIENT_SECRET_DESCRIPTION = "Invalid Client Secret"; + + /** Error description for invalid client id or client secret. */ String INVALID_CLIENT_ID_OR_SECRET_DESCRIPTION = "Invalid Client ID or Client Secret"; - /** The invalid redirect uri error description. */ + /** Error description for invalid redirect uri. */ String INVALID_REDIRECT_URI_DESCRIPTION = "Invalid Redirect URI"; - /** The invalid grant type error description. */ + /** Error description for missing redirect uri. */ + String MISSING_REDIRECT_URI_DESCRIPTION = "Missing Redirect URI"; + + /** Error description for invalid grant type. */ String INVALID_GRANT_TYPE_DESCRIPTION = "Invalid Grant Type"; - /** The failed token revocation error description. */ + /** Error description for missing grant type. */ + String MISSING_GRANT_TYPE_DESCRIPTION = "Missing Grant Type"; + + /** Error description for failed token revocation. */ String FAILED_TOKEN_REVOCATION_DESCRIPTION = "Token Revocation Failed"; } diff --git a/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/OAuthUtils.java b/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/OAuthUtils.java index d53929c1..568b6acf 100644 --- a/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/OAuthUtils.java +++ b/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/OAuthUtils.java @@ -20,15 +20,19 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; + import org.apache.commons.lang3.StringUtils; + import org.jasig.cas.services.RegisteredService; import org.jasig.cas.services.ServicesManager; import org.jasig.cas.support.oauth.services.OAuthRegisteredService; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; + import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.view.RedirectView; -import javax.servlet.http.HttpServletResponse; + import java.io.IOException; import java.io.PrintWriter; import java.io.UnsupportedEncodingException; @@ -36,34 +40,43 @@ import java.util.HashMap; import java.util.Map; +import javax.servlet.http.HttpServletResponse; + /** - * This class has some useful methods to output data in plain text, - * handle redirects, add parameter in url or find the right provider. + * A helpful utility class for the OAuth 2.0 implementation. + * + * This class has some useful methods including outputting data in plain text, handling redirects, add parameter to a + * url and find the right provider (i.e. registered service). * * @author Jerome Leleu - * @since 3.5.0 + * @author Longze Chen + * @since 4.1.5 */ public final class OAuthUtils { + /** Log instance for logging events, info, warnings, errors, etc. */ private static final Logger LOGGER = LoggerFactory.getLogger(OAuthUtils.class); /** - * Instantiates a new OAuth utils. + * Instantiates a new {@link OAuthUtils}. */ - private OAuthUtils() { - } + private OAuthUtils() {} /** - * Write to the output this error as json and return a null view. + * Write to the output a given error message as JSON and return a null view. * - * @param response http response - * @param error error message - * @param description error description - * @param status status code + * @param response the http response + * @param error the error message + * @param description the error description + * @param status the status code * @return a null view */ - public static ModelAndView writeJsonError(final HttpServletResponse response, final String error, final String description, - final int status) { + public static ModelAndView writeJsonError( + final HttpServletResponse response, + final String error, + final String description, + final int status + ) { final Map map = new HashMap<>(); map.put("error", error); if (description != null) { @@ -73,30 +86,33 @@ public static ModelAndView writeJsonError(final HttpServletResponse response, fi response.setContentType("application/json"); return writeText(response, new ObjectMapper().writeValueAsString(map), status); } catch (final JsonProcessingException e) { - LOGGER.error("Failed to write to json error to response", e); + LOGGER.error("Failed to write the JSON error to response", e); } return null; } /** - * Write to the output this error text and return a null view. + * Write to the output a given error text and return a null view. * - * @param response http response - * @param error error message - * @param status status code + * @param response the http response + * @param error the error message + * @param status the status code * @return a null view */ - public static ModelAndView writeTextError(final HttpServletResponse response, final String error, final int status) { + public static ModelAndView writeTextError( + final HttpServletResponse response, + final String error, final int status + ) { response.setContentType("text/plain"); return OAuthUtils.writeText(response, "error=" + error, status); } /** - * Write to the output the text and return a null view. + * Write to the output a given text and return a null view. * - * @param response http response - * @param text output text - * @param status status code + * @param response the http response + * @param text the output text + * @param status the status code * @return a null view */ public static ModelAndView writeText(final HttpServletResponse response, final String text, final int status) { @@ -104,7 +120,7 @@ public static ModelAndView writeText(final HttpServletResponse response, final S response.setStatus(status); printWriter.print(text); } catch (final IOException e) { - LOGGER.error("Failed to write to response", e); + LOGGER.error("Failed to write the text to response", e); } return null; } @@ -112,9 +128,9 @@ public static ModelAndView writeText(final HttpServletResponse response, final S /** * Return a view which is a redirection to an url with an error parameter. * - * @param url redirect url - * @param error error message - * @return A view which is a redirection to an url with an error parameter + * @param url the redirect url + * @param error the error message + * @return a view which is a redirection to an url with an error parameter */ public static ModelAndView redirectToError(final String url, final String error) { String useUrl = url; @@ -127,8 +143,8 @@ public static ModelAndView redirectToError(final String url, final String error) /** * Return a view which is a redirection to an url. * - * @param url redirect url - * @return A view which is a redirection to an url + * @param url the redirect url + * @return a view which is a redirection to an url */ public static ModelAndView redirectTo(final String url) { return new ModelAndView(new RedirectView(url)); @@ -137,10 +153,10 @@ public static ModelAndView redirectTo(final String url) { /** * Add a parameter with given name and value to an url. * - * @param url url to which parameters will be added - * @param name name of parameter - * @param value parameter value - * @return the url with the parameter + * @param url the url to which the parameter will be added + * @param name the name of the parameter + * @param value the value of the parameter + * @return the updated url with the parameter */ public static String addParameter(final String url, final String name, final String value) { final StringBuilder sb = new StringBuilder(); @@ -163,12 +179,16 @@ public static String addParameter(final String url, final String name, final Str } /** - * Locate the requested instance of {@link OAuthRegisteredService} by the given clientId. - * @param servicesManager the service registry DAO instance. - * @param clientId the client id by which the {@link OAuthRegisteredService} is to be located. - * @return null, or the located {@link OAuthRegisteredService} instance in the service registry. + * Locate the requested instance of {@link OAuthRegisteredService} by the given client id. + * + * @param servicesManager the service manager + * @param clientId the client id by which the {@link OAuthRegisteredService} is to be located + * @return null, or the located {@link OAuthRegisteredService} instance in the service registry */ - public static OAuthRegisteredService getRegisteredOAuthService(final ServicesManager servicesManager, final String clientId) { + public static OAuthRegisteredService getRegisteredOAuthService( + final ServicesManager servicesManager, + final String clientId + ) { for (final RegisteredService registeredService : servicesManager.getAllServices()) { if (registeredService instanceof OAuthRegisteredService) { final OAuthRegisteredService oAuthRegisteredService = (OAuthRegisteredService) registeredService; diff --git a/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/authentication/handler/support/OAuthCredentialsAuthenticationHandler.java b/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/authentication/handler/support/OAuthCredentialsAuthenticationHandler.java index bc42a800..b7b9bc8d 100644 --- a/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/authentication/handler/support/OAuthCredentialsAuthenticationHandler.java +++ b/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/authentication/handler/support/OAuthCredentialsAuthenticationHandler.java @@ -28,10 +28,16 @@ import java.security.GeneralSecurityException; /** - * Wraps the existing user authentication in an OAuth specific credential. + * OAuth authentication handler, which wraps the existing user authentication in an OAuth specific credential. + * + * The policy-based authentication manager {@link org.jasig.cas.authentication.PolicyBasedAuthenticationManager} first + * calls the {@link #supports} method to check whether the credential provided is for the CAS OAuth Service. If so, it + * then uses the {@link #authenticate} method to perform the authentication. Otherwise, the manager simply moves on to + * the next authentication handler if there is any. * * @author Michael Haselton - * @since 4.1.0 + * @author Longze Chen + * @since 4.1.5 */ public final class OAuthCredentialsAuthenticationHandler extends AbstractAuthenticationHandler { @@ -39,8 +45,11 @@ public final class OAuthCredentialsAuthenticationHandler extends AbstractAuthent public HandlerResult authenticate(final Credential credential) throws GeneralSecurityException { final OAuthCredential c = (OAuthCredential) credential; - return new DefaultHandlerResult(this, new BasicCredentialMetaData(credential), - this.principalFactory.createPrincipal(c.getId(), c.getAttributes())); + return new DefaultHandlerResult( + this, + new BasicCredentialMetaData(credential), + this.principalFactory.createPrincipal(c.getId(), c.getAttributes()) + ); } @Override diff --git a/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/authentication/principal/OAuthAuthenticationMetaDataPopulator.java b/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/authentication/principal/OAuthAuthenticationMetaDataPopulator.java index 2a2914f1..37db7692 100644 --- a/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/authentication/principal/OAuthAuthenticationMetaDataPopulator.java +++ b/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/authentication/principal/OAuthAuthenticationMetaDataPopulator.java @@ -23,14 +23,19 @@ import org.jasig.cas.authentication.Credential; /** - * Determines if the credential provided are for OAuth Services and then sets the appropriate - * Authentication attribute. + * OAuth authentication metadata populator. + * + * The policy-based authentication manager {@link org.jasig.cas.authentication.PolicyBasedAuthenticationManager} first + * calls the {@link #supports} method to check whether the credential provided is for the CAS OAuth Service. If so, it + * then uses the {@link #populateAttributes} method to set the appropriate attributes for the authentication object + * {@link org.jasig.cas.authentication.ImmutableAuthentication}. Otherwise, the manager moves on to the next metadata + * populator if there is any. * * @author Michael Haselton - * @since 4.1.0 + * @author Longze Chen + * @since 4.1.5 */ -public final class OAuthAuthenticationMetaDataPopulator implements - AuthenticationMetaDataPopulator { +public final class OAuthAuthenticationMetaDataPopulator implements AuthenticationMetaDataPopulator { @Override public void populateAttributes(final AuthenticationBuilder builder, final Credential credential) { diff --git a/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/authentication/principal/OAuthCredential.java b/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/authentication/principal/OAuthCredential.java index 8d60a798..17b2af88 100644 --- a/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/authentication/principal/OAuthCredential.java +++ b/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/authentication/principal/OAuthCredential.java @@ -25,16 +25,26 @@ import java.util.Map; /** - * OAuth Credential. + * OAuth credential. + * + * As an extension of the built-in credential {@link Credential}, this class is used to generate the ticket granting + * ticket during the process of granting three types of OAuth tokens: ONLINE access token, OFFLINE refresh token and + * PERSONAL access token. + * + * In addition, since the OAuth credential rely on the primary CAS authentication, this class wraps the existing + * authorization so that we can apply specific expiration policies based on the access type {@link #accessType}. For + * more information, see {@link org.jasig.cas.support.oauth.ticket.support.OAuthDelegatingExpirationPolicy}. * * @author Michael Haselton - * @since 4.1.0 + * @author Longze Chen + * @since 4.1.5 */ public final class OAuthCredential implements Credential { /** Authentication attribute name for access type. **/ public static final String AUTHENTICATION_ATTRIBUTE_ACCESS_TYPE = "oAuthAccessType"; + /** Unique id for serialization. */ private static final long serialVersionUID = -98723987239832729L; private final String id; @@ -44,21 +54,17 @@ public final class OAuthCredential implements Credential { private final TokenType accessType; /** - * Instantiates a new OAuth credential. - * Since oauth credentials rely on the primary authentication we wrapping the - * existing authorization so we can apply specific expiration policies + * Instantiates a new {@link OAuthCredential}. * * @param id the user id * @param accessType the access type */ public OAuthCredential(final String id, final TokenType accessType) { - this(id, new HashMap(), accessType); + this(id, new HashMap<>(), accessType); } /** - * Instantiates a new OAuth credential. - * Since oauth credentials rely on the primary authentication we wrapping the - * existing authorization so we can apply specific expiration policies + * Instantiates a new {@link OAuthCredential}. * * @param id the user id * @param attributes the attributes @@ -79,7 +85,7 @@ public String getId() { return this.id; } - public TokenType getAccessType() { + TokenType getAccessType() { return this.accessType; } diff --git a/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/metadata/ClientMetadata.java b/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/metadata/ClientMetadata.java index f5fa7e98..11f0512d 100644 --- a/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/metadata/ClientMetadata.java +++ b/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/metadata/ClientMetadata.java @@ -19,24 +19,33 @@ package org.jasig.cas.support.oauth.metadata; /** - * Client Metadata. + * Client metadata. + * + * The metadata about an OAuth registered service, which is the CAS perspective of an OSF developer app. In addition, + * the property {@link ClientMetadata#users} is not a list of users as its name indicates but actually the total number + * of users that have authorized the client / service / developer app. + * + * The CAS OAuth Service {@link org.jasig.cas.support.oauth.CentralOAuthServiceImpl#getClientMetadata} uses this class + * when retrieving the metadata about a given client / service / developer app. * * @author Michael Haselton - * @since 4.1.0 + * @author Longze Chen + * @since 4.1.5 */ public class ClientMetadata { + private final String clientId; private final String name; private final String description; private final Integer users; /** - * Constructors a new client metadata class. + * Instantiate a {@link ClientMetadata} with details of an OAuth registered service. * - * @param clientId the client id. - * @param name the name. - * @param description the description. - * @param users the users. + * @param clientId the client id + * @param name the name of the client + * @param description the description of the client + * @param users the number of users of the client */ public ClientMetadata(final String clientId, final String name, final String description, final Integer users) { this.clientId = clientId; diff --git a/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/metadata/PrincipalMetadata.java b/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/metadata/PrincipalMetadata.java index f43d5115..f75a2b53 100644 --- a/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/metadata/PrincipalMetadata.java +++ b/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/metadata/PrincipalMetadata.java @@ -22,12 +22,27 @@ import java.util.Set; /** - * Principal Metadata. + * Principal metadata. + * + * The names of the class and its properties are not descriptive at all (if not confusing). The principal metadata + * itself contains neither the principal id (i.e. who the user is) nor the authorization id (i.e. the access token). + * It only contains the service details (the id, name and description of the client and the scopes authorized) of an + * given authorization of a certain principal. + * + * I guess the primary reason that this class was originally designed with the name "Principal Metadata" is that it is + * only used to retrieve the full metadata about all authorized clients of a given principal in the CAS OAuth Service + * {@link org.jasig.cas.support.oauth.CentralOAuthServiceImpl#getPrincipalMetadata}. + * + * In addition, only an access token of {@link org.jasig.cas.support.oauth.token.TokenType#CAS} is allowed to be used + * for retrieving the full metadata of a principal. Such a token is issued only during the CAS authentication service + * validation phase by {@link org.jasig.cas.support.oauth.web.OAuth20ServiceValidateController}. * * @author Michael Haselton - * @since 4.1.0 + * @author Longze Chen + * @since 4.1.5 */ public class PrincipalMetadata { + private final String clientId; private final String name; private final String description; @@ -36,9 +51,9 @@ public class PrincipalMetadata { /** * Constructs a new principal metadata class. * - * @param clientId the client id. - * @param name the name. - * @param description the description. + * @param clientId the client id + * @param name the client name + * @param description the client description */ public PrincipalMetadata(final String clientId, final String name, final String description) { this.clientId = clientId; diff --git a/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/personal/PersonalAccessToken.java b/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/personal/PersonalAccessToken.java index 63268a14..7fdeb238 100644 --- a/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/personal/PersonalAccessToken.java +++ b/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/personal/PersonalAccessToken.java @@ -21,26 +21,28 @@ import java.util.Set; /** - * Personal Access Token. + * Personal access token (PAT). + * + * This is not a model class but a helper. It stores the token information retrieved from the OSF database, of which + * the model class is {@link io.cos.cas.adaptors.postgres.models.OpenScienceFrameworkApiOauth2PersonalAccessToken}. + * + * The CAS OAuth Service {@link org.jasig.cas.support.oauth.CentralOAuthServiceImpl} uses this information to create + * its own PAT, of which the model class is {@link org.jasig.cas.support.oauth.token.AccessTokenImpl} and of which the + * token type {@link org.jasig.cas.support.oauth.token.TokenType} is PERSONAL - 2. * * @author Michael Haselton - * @since 4.1.0 + * @author Longze Chen + * @since 4.1.5 */ public final class PersonalAccessToken { - /** - * The id of the token. - */ + /** The id of the token. */ private final String id; - /** - * The principal id of the token. - */ + /** The principal id of the token. */ private final String principalId; - /** - * The scopes assigned to the token. - */ + /** The scopes assigned to the token. */ private final Set scopes; /** @@ -59,7 +61,7 @@ public PersonalAccessToken(final String id, final String principalId, final Set< /** * Get the identifier of the token. * - * @return the id. + * @return the the id of the token */ public String getId() { return this.id; @@ -68,7 +70,7 @@ public String getId() { /** * Get the principal id of the token. * - * @return the principal id. + * @return the principal id */ public String getPrincipalId() { return this.principalId; @@ -77,7 +79,7 @@ public String getPrincipalId() { /** * Get a set of scopes assigned to the token. * - * @return a set of scopes. + * @return a set of scopes */ public Set getScopes() { return this.scopes; diff --git a/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/personal/PersonalAccessTokenManager.java b/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/personal/PersonalAccessTokenManager.java index bc14b5b1..cdddc34e 100644 --- a/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/personal/PersonalAccessTokenManager.java +++ b/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/personal/PersonalAccessTokenManager.java @@ -21,22 +21,25 @@ import org.jasig.cas.support.oauth.personal.handler.support.PersonalAccessTokenHandler; /** - * Personal Access Token Manager. + * Personal access token manager. + * + * This PAT manager is a property of the CAS OAuth Service {@link org.jasig.cas.support.oauth.CentralOAuthServiceImpl} + * and is used to retrieve PATs via its PAT handler. With current CAS settings, the OSF PAT handler is the one being + * used. See {@link io.cos.cas.adaptors.postgres.handlers.OpenScienceFrameworkPersonalAccessTokenHandler} for details. * * @author Michael Haselton - * @since 4.1.0 + * @author Longze Chen + * @since 4.1.5 */ public class PersonalAccessTokenManager { - /** - * An instance of the personal access token handler. - */ + /** An instance of the personal access token handler. */ private final PersonalAccessTokenHandler personalAccessTokenHandler; /** - * Constructs a new personal access token manager w/ the handler specified. + * Constructs a new personal access token manager with the handler specified. * - * @param personalAccessTokenHandler the handler. + * @param personalAccessTokenHandler the handler */ public PersonalAccessTokenManager(final PersonalAccessTokenHandler personalAccessTokenHandler) { this.personalAccessTokenHandler = personalAccessTokenHandler; @@ -45,8 +48,8 @@ public PersonalAccessTokenManager(final PersonalAccessTokenHandler personalAcces /** * Get a personal access token from the handler by the token id specified. * - * @param tokenId the token id. - * @return a personal access token or null. + * @param tokenId the token id + * @return a personal access token or null */ public PersonalAccessToken getToken(final String tokenId) { return personalAccessTokenHandler.getToken(tokenId); diff --git a/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/personal/handler/SimplePersonalAccessTokenHandler.java b/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/personal/handler/SimplePersonalAccessTokenHandler.java index 8239b58b..03101d32 100644 --- a/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/personal/handler/SimplePersonalAccessTokenHandler.java +++ b/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/personal/handler/SimplePersonalAccessTokenHandler.java @@ -25,29 +25,30 @@ import java.util.Set; /** - * Simple Personal Token Handler. + * Simple personal access token handler. + * + * This handler is not used by the PAT manager {@link org.jasig.cas.support.oauth.personal.PersonalAccessTokenManager}, + * which uses {@link io.cos.cas.adaptors.postgres.handlers.OpenScienceFrameworkPersonalAccessTokenHandler} instead with + * current CAS settings. * * @author Michael Haselton - * @since 4.1.0 + * @author Longze Chen + * @since 4.1.5 */ public class SimplePersonalAccessTokenHandler extends AbstractPersonalAccessTokenHandler { - /** - * A list of personal access tokens. - */ + /** A list of personal access tokens. */ private final Set tokens; - /** - * Constructs a new instance of simple personal access token handler without any assigned tokens. - */ + /** Constructs a new instance of simple personal access token handler without any assigned tokens. */ public SimplePersonalAccessTokenHandler() { - this(new HashSet()); + this(new HashSet<>()); } /** * Constructs a new instance of simple personal access token handler with the set of tokens specified. * - * @param tokens the set of tokens. + * @param tokens the set of personal access tokens */ public SimplePersonalAccessTokenHandler(final Set tokens) { this.tokens = tokens; diff --git a/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/personal/handler/support/AbstractPersonalAccessTokenHandler.java b/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/personal/handler/support/AbstractPersonalAccessTokenHandler.java index 1e21b452..d75ab532 100644 --- a/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/personal/handler/support/AbstractPersonalAccessTokenHandler.java +++ b/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/personal/handler/support/AbstractPersonalAccessTokenHandler.java @@ -19,10 +19,10 @@ package org.jasig.cas.support.oauth.personal.handler.support; /** - * Base class for Personal Token Handler. + * The abstract base class for personal access token handler classes. * * @author Michael Haselton - * @since 4.1.0 + * @author Longze Chen + * @since 4.1.5 */ -public abstract class AbstractPersonalAccessTokenHandler implements PersonalAccessTokenHandler { -} +public abstract class AbstractPersonalAccessTokenHandler implements PersonalAccessTokenHandler {} diff --git a/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/personal/handler/support/PersonalAccessTokenHandler.java b/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/personal/handler/support/PersonalAccessTokenHandler.java index ccee0257..c4208460 100644 --- a/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/personal/handler/support/PersonalAccessTokenHandler.java +++ b/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/personal/handler/support/PersonalAccessTokenHandler.java @@ -21,18 +21,19 @@ import org.jasig.cas.support.oauth.personal.PersonalAccessToken; /** - * Interface for Personal Token Handler. + * The interface for personal access token handler classes. * * @author Michael Haselton - * @since 4.1.0 + * @author Longze Chen + * @since 4.1.5 */ public interface PersonalAccessTokenHandler { /** * Get a personal access token from the handler by the token id specified. * - * @param tokenId the token id. - * @return a personal access token or null. + * @param tokenId the token id + * @return a personal access token or null */ PersonalAccessToken getToken(String tokenId); } diff --git a/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/scope/InvalidScopeException.java b/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/scope/InvalidScopeException.java index 99e1c26b..f6966def 100644 --- a/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/scope/InvalidScopeException.java +++ b/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/scope/InvalidScopeException.java @@ -24,10 +24,12 @@ * The exception to throw when we cannot find a requested scope. * * @author Michael Haselton - * @since 4.1.0 + * @author Longze Chen + * @since 4.1.5 */ public class InvalidScopeException extends RootCasException { + /** Unique id for serialization. */ private static final long serialVersionUID = -8143505357364890832L; /** The code description. */ @@ -36,8 +38,9 @@ public class InvalidScopeException extends RootCasException { private final String scope; /** - * Constructs a InvalidScopeException with the default exception code. - * @param scope the scope that originally caused this exception to be thrown. + * Instantiates a {@link InvalidScopeException} with the default exception code. + * + * @param scope the scope that originally caused this exception to be thrown */ public InvalidScopeException(final String scope) { super(CODE); @@ -45,11 +48,10 @@ public InvalidScopeException(final String scope) { } /** - * Constructs a InvalidScopeException with the default exception code and - * the original exception that was thrown. + * Instantiates a {@link InvalidScopeException} with the default exception code and the original exception. * * @param throwable the chained exception - * @param scope the scope that originally caused this exception to be thrown. + * @param scope the scope that originally caused this exception to be thrown */ public InvalidScopeException(final Throwable throwable, final String scope) { super(CODE, throwable); @@ -58,6 +60,7 @@ public InvalidScopeException(final Throwable throwable, final String scope) { /** * Returns the message of this exception. + * * @return the message * @see InvalidScopeException#scope */ diff --git a/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/scope/Scope.java b/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/scope/Scope.java index a236a53e..71f45994 100644 --- a/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/scope/Scope.java +++ b/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/scope/Scope.java @@ -19,10 +19,21 @@ package org.jasig.cas.support.oauth.scope; /** - * An OAuth Scope. + * OAuth scope. + * + * In CAS, the OAuth scope is not stored in database by itself but as a Large Object (LOB) property of the token or + * the code it is associated with. See {@link org.jasig.cas.support.oauth.token.AbstractToken} for details. + * + * In OSF, the OAuth scope is stored in the database and the token-scope association is defined by a M2M relationship. + * For more information, refer to the following three model classes. + * + * Scope: {@literal io.cos.cas.adaptors.postgres.handlers.OpenScienceFrameworkApiOauth2Scope} + * Token: {@literal io.cos.cas.adaptors.postgres.handlers.OpenScienceFrameworkApiOauth2PersonalAccessToken} + * M2M Relationship {@literal io.cos.cas.adaptors.postgres.handlers.OpenScienceFrameworkApiOauth2TokenScope} * * @author Michael Haselton - * @since 4.1.0 + * @author Longze Chen + * @since 4.1.5 */ public final class Scope { @@ -36,10 +47,10 @@ public final class Scope { private final Boolean isDefault; /** - * Creates a new scope. + * Creates a new and non-default scope. * - * @param name the scope name.. - * @param description the scope description. + * @param name the scope name + * @param description the scope description */ public Scope(final String name, final String description) { this(name, description, Boolean.FALSE); @@ -48,9 +59,9 @@ public Scope(final String name, final String description) { /** * Creates a new scope. * - * @param name the scope name.. - * @param description the scope description. - * @param isDefault the scope default status. + * @param name the scope name + * @param description the scope description + * @param isDefault the scope default status */ public Scope(final String name, final String description, final Boolean isDefault) { this.name = name; @@ -61,7 +72,7 @@ public Scope(final String name, final String description, final Boolean isDefaul /** * Get the name of the scope. * - * @return the name. + * @return the scope name */ public String getName() { return this.name; @@ -70,7 +81,7 @@ public String getName() { /** * Get the description of the scope. * - * @return the description. + * @return the scope description */ public String getDescription() { return this.description; @@ -79,7 +90,7 @@ public String getDescription() { /** * Get the default status of the scope. * - * @return the default status. + * @return the scope default status */ public Boolean getIsDefault() { return this.isDefault; diff --git a/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/scope/ScopeManager.java b/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/scope/ScopeManager.java index 28564b84..4557427f 100644 --- a/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/scope/ScopeManager.java +++ b/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/scope/ScopeManager.java @@ -25,10 +25,20 @@ import java.util.Set; /** - * Scope Manager. + * OAuth scope manager. + * + * This manager is a property of the CAS OAuth Service {@link org.jasig.cas.support.oauth.CentralOAuthServiceImpl} and + * is used to retrieve scope information via its two scope handlers. + * + * The OSF scope handler {@literal io.cos.cas.adaptors.postgres.handlers.OpenScienceFrameworkPersonalAccessTokenHandler} + * is used as the main scope handler with current CAS settings. + * + * The simple scope handler {@link org.jasig.cas.support.oauth.scope.handler.SimpleScopeHandler} is used as the CAS + * scope handler by by default. * * @author Michael Haselton - * @since 4.1.0 + * @author Longze Chen + * @since 4.1.5 */ public class ScopeManager { @@ -41,17 +51,17 @@ public class ScopeManager { /** * Creates a new scope manager with only a default scope handler. * - * @param scopeHandler The default scope handler. + * @param scopeHandler The default scope handler */ public ScopeManager(final ScopeHandler scopeHandler) { this(scopeHandler, new SimpleScopeHandler()); } /** - * Creates a new scope manager with the addition of the cas scope handler. + * Creates a new scope manager with a scope handler and an additional cas scope handler. * - * @param scopeHandler The default scope handler. - * @param casScopeHandler The cas scope handler. + * @param scopeHandler The default scope handler + * @param casScopeHandler The cas scope handler */ public ScopeManager(final ScopeHandler scopeHandler, final ScopeHandler casScopeHandler) { this.scopeHandler = scopeHandler; @@ -61,8 +71,8 @@ public ScopeManager(final ScopeHandler scopeHandler, final ScopeHandler casScope /** * Retrieve a scope by name. * - * @param name the name of the scope. - * @return the retireved scope + * @param name the name of the scope + * @return the retrieved scope */ public Scope getScope(final String name) { return scopeHandler.getScope(name); @@ -71,7 +81,7 @@ public Scope getScope(final String name) { /** * Retrieve a set of default scopes. * - * @return the set of scopes. + * @return the set of scopes */ public Set getDefaults() { return scopeHandler.getDefaults(); @@ -80,7 +90,7 @@ public Set getDefaults() { /** * Retrieve a set of scopes specific to the CAS handler. * - * @return the set of scopes. + * @return the set of scopes */ public Set getCASScopes() { final Set scopeSet = new HashSet<>(); diff --git a/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/scope/handler/SimpleScopeHandler.java b/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/scope/handler/SimpleScopeHandler.java index f7bfff48..54093d01 100644 --- a/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/scope/handler/SimpleScopeHandler.java +++ b/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/scope/handler/SimpleScopeHandler.java @@ -25,29 +25,30 @@ import java.util.Set; /** - * Simple OAuth Scope Handler. + * Simple OAuth scope handler. + * + * With current CAS settings, this handler is not used as the primary scope handler but only as the CAS scope handler + * in the scope manager {@link org.jasig.cas.support.oauth.scope.ScopeManager}, which uses the OSF scope handler + * {@literal io.cos.cas.adaptors.postgres.handlers.OpenScienceFrameworkScopeHandler} as its primary one. * * @author Michael Haselton - * @since 4.1.0 + * @author Longze Chen + * @since 4.1.5 */ public class SimpleScopeHandler extends AbstractScopeHandler { - /** - * The set of scopes added to the handler. - */ + /** The set of scopes added to the handler. */ private final Set scopes; - /** - * Constructs a new instance of the scope handler w/ a blank list of scopes. - */ + /** Constructs a new instance of the scope handler with a blank list of scopes. */ public SimpleScopeHandler() { - this(new HashSet()); + this(new HashSet<>()); } /** - * Constructors a new instance of the scope handler w/ the scopes specified. + * Constructors a new instance of the scope handler with the list of scopes specified. * - * @param scopes the list of scopes. + * @param scopes the list of scopes */ public SimpleScopeHandler(final Set scopes) { this.scopes = scopes; diff --git a/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/scope/handler/support/AbstractScopeHandler.java b/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/scope/handler/support/AbstractScopeHandler.java index 0873af6f..64e2a637 100644 --- a/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/scope/handler/support/AbstractScopeHandler.java +++ b/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/scope/handler/support/AbstractScopeHandler.java @@ -24,10 +24,11 @@ import java.util.Set; /** - * Base class for OAuth Scope Handlers. + * The abstract base class for OAuth scope handler classes. * * @author Michael Haselton - * @since 4.1.0 + * @author Longze Chen + * @since 4.1.5 */ public abstract class AbstractScopeHandler implements ScopeHandler { diff --git a/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/scope/handler/support/ScopeHandler.java b/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/scope/handler/support/ScopeHandler.java index b5e0eba4..b148db48 100644 --- a/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/scope/handler/support/ScopeHandler.java +++ b/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/scope/handler/support/ScopeHandler.java @@ -23,17 +23,18 @@ import java.util.Set; /** - * Interface for OAuth Scope Handlers. + * The interface for OAuth scope handler classes. * * @author Michael Haselton - * @since 4.1.0 + * @author Longze Chen + * @since 4.1.5 */ public interface ScopeHandler { /** * Get the scope specified by name. * - * @param name the name of the scope. + * @param name the name of the scope * @return the scope */ Scope getScope(String name); @@ -41,7 +42,7 @@ public interface ScopeHandler { /** * Get the list of default scopes. * - * @return a list of default scopes. + * @return a list of default scopes */ Set getDefaults(); } diff --git a/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/services/OAuthRegisteredService.java b/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/services/OAuthRegisteredService.java index 5a1c638c..8fb6ad3c 100644 --- a/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/services/OAuthRegisteredService.java +++ b/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/services/OAuthRegisteredService.java @@ -21,12 +21,16 @@ import org.apache.commons.lang3.builder.CompareToBuilder; import org.apache.commons.lang3.builder.HashCodeBuilder; import org.apache.commons.lang3.builder.ToStringBuilder; + import org.jasig.cas.services.OSFRegisteredService; import org.jasig.cas.services.RegisteredService; /** - * An extension of the {@link OSFRegisteredService} that defines - * the OAuth client id and secret for a given registered service. + * OAuth registered service. + * + * As an extension of the {@link OSFRegisteredService}, this class defines a few extra OAuth properties for a given + * registered service, including the client id, client secret and a boolean flag which determines whether to bypass + * the approval prompt during authorization. * * @author Misagh Moayyed * @author Michael Haselton @@ -35,6 +39,7 @@ */ public final class OAuthRegisteredService extends OSFRegisteredService { + /** Unique id for serialization. */ private static final long serialVersionUID = 5318897374067731021L; private String clientSecret; @@ -105,9 +110,6 @@ public boolean equals(final Object obj) { @Override public int hashCode() { - return new HashCodeBuilder() - .appendSuper(super.hashCode()) - .append(clientId) - .toHashCode(); + return new HashCodeBuilder().appendSuper(super.hashCode()).append(clientId).toHashCode(); } } diff --git a/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/ticket/support/OAuthDelegatingExpirationPolicy.java b/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/ticket/support/OAuthDelegatingExpirationPolicy.java index f00af1f7..eaa4bce6 100644 --- a/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/ticket/support/OAuthDelegatingExpirationPolicy.java +++ b/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/ticket/support/OAuthDelegatingExpirationPolicy.java @@ -30,15 +30,42 @@ import javax.validation.constraints.NotNull; /** - * Delegates to different expiration policies depending on oauth - * token type specified by the credential. + * Delegates to different expiration policies depending on the OAuth token type {@link TokenType} specified by the + * OAuth credential access type {@literal OAuthCredential#accessType}. The primary CAS authentication (i.e. the CAS + * Auth Service, as opposed to the CAS OAuth Service) {@link org.jasig.cas.CentralAuthenticationServiceImpl} has two + * policies using the class, {@code ticketGrantingTicketExpirationPolicy} and {@code serviceTicketExpirationPolicy}. + * + * For both TGT and ST, the refresh token policy {@link #oAuthRefreshTokenExpirationPolicy} uses the built-in + * never-expire policy {@link org.jasig.cas.ticket.support.NeverExpiresExpirationPolicy}. Thus, the refresh token + * never expires. + * + * For both TGT and ST, the ONLINE / OFFLINE access token policy {@link #oAuthAccessTokenExpirationPolicy} uses the + * built-in time-out policy {@link org.jasig.cas.ticket.support.TimeoutExpirationPolicy}, where + * {@code timeToKillInMilliSeconds} is set to 3,600,000 milliseconds. Thus, both ONLINE and OFFLINE access tokens + * expire after 1 hour. + * + * The PERSONAL access token never expires. Instead of using the built-in never-expire policy, this class simply let + * the expiration check method {@link #isExpired} always return {@code False}. + * + * The session policy differs between TGT and ST. This policy affects the expiration time for CAS access token (which + * is determined by the TGT) and the time for ONLINE / OFFLINE authorization code (which is determined by the ST). + * + * 1) ST uses the built-in {@link org.jasig.cas.ticket.support.MultiTimeUseOrTimeoutExpirationPolicy}, where + * {@code timeToKill} is set to 60 seconds. Thus, both ONLINE and OFFLINE authorization codes expire in 1 minute. + * + * 2) For TGTs, which policy to use depends on whether the "Remember Me" or "Stay Signed In" option is selected or not + * at login. If selected, {@link org.jasig.cas.ticket.support.TimeoutExpirationPolicy} is used with + * {@code timeToKillInMilliSeconds} set to 2,592,000 milliseconds, which is 30 days. Otherwise, + * {@link org.jasig.cas.ticket.support.TicketGrantingTicketExpirationPolicy} is used with a 2-hour sliding expiration + * with 8-hour maximum lifetime. * * @author Michael Haselton - * @since 4.1.0 + * @author Longze Chen + * @since 4.1.5 */ public final class OAuthDelegatingExpirationPolicy extends AbstractCasExpirationPolicy { - /** Serialization support. */ + /** Unique id for serialization. */ private static final long serialVersionUID = 4461752518354198401L; @NotNull @@ -52,36 +79,38 @@ public final class OAuthDelegatingExpirationPolicy extends AbstractCasExpiration @Override public boolean isExpired(final TicketState ticketState) { + + final Authentication authentication; final AbstractTicket ticket = (AbstractTicket) ticketState; final TicketGrantingTicket ticketGrantingTicket = ticket.getGrantingTicket(); - final Authentication authentication; if (ticketGrantingTicket != null) { authentication = ticketGrantingTicket.getAuthentication(); } else { authentication = ticket.getAuthentication(); } - final TokenType tokenType = (TokenType) authentication.getAttributes() - .get(OAuthCredential.AUTHENTICATION_ATTRIBUTE_ACCESS_TYPE); + final TokenType tokenType + = (TokenType) authentication.getAttributes().get(OAuthCredential.AUTHENTICATION_ATTRIBUTE_ACCESS_TYPE); - // offline + // OFFLINE refresh token never expires; OFFLINE access token expires after 1 hour if (tokenType == TokenType.OFFLINE) { - return ticket instanceof TicketGrantingTicket ? oAuthRefreshTokenExpirationPolicy.isExpired(ticketState) + return ticket instanceof TicketGrantingTicket + ? oAuthRefreshTokenExpirationPolicy.isExpired(ticketState) : oAuthAccessTokenExpirationPolicy.isExpired(ticketState); } - // online + // ONLINE access token expires after 1 hour if (tokenType == TokenType.ONLINE && ticket instanceof TicketGrantingTicket) { return oAuthAccessTokenExpirationPolicy.isExpired(ticketState); } - // personal + // PERSONAL access token never expires if (tokenType == TokenType.PERSONAL && ticket instanceof TicketGrantingTicket) { return false; } - // service validation / other + // Service validation and other, expiration time varies return sessionExpirationPolicy.isExpired(ticketState); } diff --git a/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/token/AbstractToken.java b/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/token/AbstractToken.java index 6beef66e..18697150 100644 --- a/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/token/AbstractToken.java +++ b/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/token/AbstractToken.java @@ -26,22 +26,24 @@ import javax.persistence.MappedSuperclass; import javax.persistence.PrePersist; import javax.persistence.PreUpdate; + import java.util.HashSet; import java.util.Set; /** - * Abstract base class for Token classes. + * The abstract base class for token classes. * * @author Michael Haselton - * @since 4.1.0 + * @author Longze Chen + * @since 4.1.5 */ @MappedSuperclass public abstract class AbstractToken implements Token { - /** Unique Id for serialization. */ + /** Unique id for serialization. */ private static final long serialVersionUID = -5608324980180191592L; - /** The unique identifier for this token. */ + /** The unique id for this token. */ @Id @Column(name="ID", nullable=false) private String id; @@ -65,30 +67,29 @@ public abstract class AbstractToken implements Token { @Column(name="SCOPES_HASH", nullable=false) private Integer scopesHash; - /** - * Instantiates a new abstract token. - */ - protected AbstractToken() { - // nothing to do - } + /** Default constructor. */ + protected AbstractToken(){} /** - * Constructs a new Ticket with a unique id, a possible parent Ticket (can - * be null) and a specified Expiration Policy. + * Instantiate a new {@link AbstractToken}. * - * @param id the unique identifier for the token - * @param clientId the client identifier for the token - * @param principalId the principal identifier for the token - * @param type the type of the token - * @param scopes the assigned scopes for the token + * @param id the unique id of the token + * @param clientId the client id + * @param principalId the principal id + * @param type the token type + * @param scopes the token scopes */ - public AbstractToken(final String id, final String clientId, final String principalId, final TokenType type, - final Set scopes) { - Assert.notNull(id, "id cannot be null"); - Assert.notNull(principalId, "principalId cannot be null"); - Assert.notNull(type, "type cannot be null"); - Assert.notNull(scopes, "scopes cannot be null"); - + public AbstractToken( + final String id, + final String clientId, + final String principalId, + final TokenType type, + final Set scopes + ) { + Assert.notNull(id, "Token ID cannot be null"); + Assert.notNull(principalId, "Principal ID cannot be null"); + Assert.notNull(type, "Token type cannot be null"); + Assert.notNull(scopes, "Token scopes cannot be null"); this.id = id; this.clientId = clientId; this.principalId = principalId; @@ -96,9 +97,7 @@ public AbstractToken(final String id, final String clientId, final String princi this.scopes.addAll(scopes); } - /** - * Compute the hash of all scopes upon saving the token. - */ + /** Compute the hash of all scopes upon saving the token. */ @PreUpdate @PrePersist private void updateScopesHash() { diff --git a/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/token/AccessToken.java b/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/token/AccessToken.java index 4a55eda6..de0c120b 100644 --- a/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/token/AccessToken.java +++ b/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/token/AccessToken.java @@ -19,40 +19,41 @@ package org.jasig.cas.support.oauth.token; import org.jasig.cas.authentication.principal.Service; - import org.jasig.cas.ticket.ServiceTicket; - import org.jasig.cas.ticket.TicketGrantingTicket; +import org.jasig.cas.ticket.ServiceTicket; +import org.jasig.cas.ticket.TicketGrantingTicket; /** - * Access Token. + * The interface for OAuth Access Token. * * @author Michael Haselton - * @since 4.1.0 + * @author Longze Chen + * @since 4.1.5 */ public interface AccessToken extends Token { - /** Prefix generally applied to unique ids generated - * by {@link org.jasig.cas.util.UniqueTicketIdGenerator}. - **/ + /** + * Prefix generally applied to unique ids generated by {@link org.jasig.cas.util.UniqueTicketIdGenerator}. + */ String PREFIX = "AT"; /** * Retrieves the ticket granting ticket. * - * @return the requested ticket granting ticket. + * @return the requested ticket granting ticket */ TicketGrantingTicket getTicketGrantingTicket(); /** * Retrieves the service. * - * @return the requested service. + * @return the requested service */ Service getService(); /** * Retrieves the service ticket. * - * @return the requested service ticket. + * @return the requested service ticket */ ServiceTicket getServiceTicket(); } diff --git a/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/token/AccessTokenImpl.java b/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/token/AccessTokenImpl.java index 23924d46..e0f63a42 100644 --- a/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/token/AccessTokenImpl.java +++ b/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/token/AccessTokenImpl.java @@ -39,7 +39,7 @@ import java.util.Set; /** - * Access Token Implementation class. + * The implementation class for {@link AccessToken}. * * @author Michael Haselton * @author Longze Chen @@ -50,35 +50,31 @@ @Access(AccessType.FIELD) public final class AccessTokenImpl extends AbstractToken implements AccessToken { - /** Unique Id for serialization. */ + /** Unique id for serialization. */ private static final long serialVersionUID = -2608145809180961597L; - /** The TicketGrantingTicket this is associated with. */ + /** The ticket granting ticket this access token is associated with. */ @OneToOne(targetEntity=TicketGrantingTicketImpl.class, orphanRemoval=true) @OnDelete(action= OnDeleteAction.CASCADE) private TicketGrantingTicket ticketGrantingTicket; - /** The service associated with the tgt. */ + /** The service associated with the ticket granting ticket. */ @Lob @Column(name="SERVICE") private Service service; - /** The ServiceTicket this is associated with. */ + /** The service ticket this access token is associated with. */ @OneToOne(targetEntity=ServiceTicketImpl.class, orphanRemoval=true) @OnDelete(action= OnDeleteAction.CASCADE) private ServiceTicket serviceTicket; - /** - * Instantiates a new access token impl. - */ - public AccessTokenImpl() { - // nothing to do - } + /** Default constructor. */ + public AccessTokenImpl(){} /** - * Constructs a new AccessToken. + * Instantiate a new {@link AccessTokenImpl}. * - * @param id the id of the Ticket + * @param id the id of the access token * @param type the token type * @param clientId the client id * @param principalId the principal id @@ -98,7 +94,6 @@ public AccessTokenImpl( final Set scopes ) { super(id, clientId, principalId, type, scopes); - this.ticketGrantingTicket = ticketGrantingTicket; this.service = service; this.serviceTicket = serviceTicket; diff --git a/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/token/AuthorizationCode.java b/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/token/AuthorizationCode.java index b468fb59..efdbbd2b 100644 --- a/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/token/AuthorizationCode.java +++ b/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/token/AuthorizationCode.java @@ -21,16 +21,17 @@ import org.jasig.cas.ticket.ServiceTicket; /** - * Interface for a Authorization Code Ticket. + * The interface for OAuth Authorization Code. * * @author Michael Haselton - * @since 4.1.0 + * @author Longze Chen + * @since 4.1.5 */ public interface AuthorizationCode extends Token { - /** Prefix generally applied to unique ids generated - * by {@link org.jasig.cas.util.UniqueTicketIdGenerator}. - **/ + /** + * Prefix generally applied to unique ids generated by {@link org.jasig.cas.util.UniqueTicketIdGenerator}. + */ String PREFIX = "AC"; /** diff --git a/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/token/AuthorizationCodeImpl.java b/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/token/AuthorizationCodeImpl.java index 64c06174..53ff4d69 100644 --- a/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/token/AuthorizationCodeImpl.java +++ b/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/token/AuthorizationCodeImpl.java @@ -20,6 +20,7 @@ import org.hibernate.annotations.OnDelete; import org.hibernate.annotations.OnDeleteAction; + import org.jasig.cas.ticket.ServiceTicket; import org.jasig.cas.ticket.ServiceTicketImpl; import org.jasig.cas.ticket.Ticket; @@ -27,46 +28,49 @@ import javax.persistence.Entity; import javax.persistence.OneToOne; import javax.persistence.Table; + import java.util.Set; /** - * Authorization Code token implementation. + * The implementation class for {@link AuthorizationCode}. * * @author Michael Haselton - * @since 4.1.0 + * @author Longze Chen + * @since 4.1.5 */ @Entity @Table(name="AUTHORIZATIONCODE") public final class AuthorizationCodeImpl extends AbstractToken implements AuthorizationCode { - /** Unique Id for serialization. */ + /** Unique id for serialization. */ private static final long serialVersionUID = -7608149809180111599L; - /** The ServiceTicket this is associated with. */ + /** The service ticket this authorization code is associated with. */ @OneToOne(targetEntity=ServiceTicketImpl.class, orphanRemoval=true) @OnDelete(action= OnDeleteAction.CASCADE) private ServiceTicket serviceTicket; - /** - * Instantiates a new oauth refresh token impl. - */ - public AuthorizationCodeImpl() { - // nothing to do - } + /** Default constructor. */ + public AuthorizationCodeImpl(){} /** - * Constructs a new Code Token. - * May throw an {@link IllegalArgumentException} if the Authentication object is null. + * Instantiate a new {@link AuthorizationCodeImpl}. * - * @param id the id of the Ticket + * @param id the id of the authorization code * @param type the token type * @param clientId the client id * @param principalId the principal id * @param serviceTicket the service ticket * @param scopes the scopes */ - public AuthorizationCodeImpl(final String id, final TokenType type, final String clientId, final String principalId, - final ServiceTicket serviceTicket, final Set scopes) { + public AuthorizationCodeImpl( + final String id, + final TokenType type, + final String clientId, + final String principalId, + final ServiceTicket serviceTicket, + final Set scopes + ) { super(id, clientId, principalId, type, scopes); this.serviceTicket = serviceTicket; } diff --git a/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/token/InvalidTokenException.java b/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/token/InvalidTokenException.java index c786ff3f..0db6e78a 100644 --- a/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/token/InvalidTokenException.java +++ b/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/token/InvalidTokenException.java @@ -24,10 +24,12 @@ * The exception to throw when we cannot verify the token. * * @author Michael Haselton - * @since 4.1.0 + * @author Longze Chen + * @since 4.1.5 */ public class InvalidTokenException extends RootCasException { + /** Unique id for serialization. */ private static final long serialVersionUID = -5875760146336410828L; /** The code description. */ @@ -36,8 +38,9 @@ public class InvalidTokenException extends RootCasException { private final String tokenId; /** - * Constructs a InvalidTokenException with the default exception code. - * @param tokenId the token id that originally caused this exception to be thrown. + * Instantiates a new {@link InvalidTokenException} with the default exception code. + * + * @param tokenId the token id that originally caused this exception to be thrown */ public InvalidTokenException(final String tokenId) { super(CODE); @@ -45,11 +48,10 @@ public InvalidTokenException(final String tokenId) { } /** - * Constructs a InvalidTokenException with the default exception code and - * the original exception that was thrown. + * Instantiates a new {@link InvalidTokenException} with the default exception code and the original exception. * * @param throwable the chained exception - * @param tokenId the token id that originally caused this exception to be thrown. + * @param tokenId the token id that originally caused this exception to be thrown */ public InvalidTokenException(final Throwable throwable, final String tokenId) { super(CODE, throwable); @@ -58,6 +60,7 @@ public InvalidTokenException(final Throwable throwable, final String tokenId) { /** * Returns the message of this exception, token is not included for security purposes. + * * @return the message * @see InvalidTokenException#tokenId */ diff --git a/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/token/RefreshToken.java b/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/token/RefreshToken.java index 6402a0e9..78249bad 100644 --- a/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/token/RefreshToken.java +++ b/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/token/RefreshToken.java @@ -22,29 +22,30 @@ import org.jasig.cas.ticket.TicketGrantingTicket; /** - * A Refresh Token. + * The interface for OAuth Refresh Token. * * @author Michael Haselton - * @since 4.1.0 + * @author Longze Chen + * @since 4.1.5 */ public interface RefreshToken extends Token { - /** Prefix generally applied to unique ids generated - * by {@link org.jasig.cas.util.UniqueTicketIdGenerator}. - **/ + /** + * Prefix generally applied to unique ids generated by {@link org.jasig.cas.util.UniqueTicketIdGenerator}. + */ String PREFIX = "RT"; /** * Retrieves the ticket granting ticket. * - * @return the requested ticket granting ticket. + * @return the requested ticket granting ticket */ TicketGrantingTicket getTicketGrantingTicket(); /** * Retrieves the service. * - * @return the requested service. + * @return the requested service */ Service getService(); } diff --git a/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/token/RefreshTokenImpl.java b/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/token/RefreshTokenImpl.java index 9be65b1d..516c4458 100644 --- a/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/token/RefreshTokenImpl.java +++ b/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/token/RefreshTokenImpl.java @@ -20,6 +20,7 @@ import org.hibernate.annotations.OnDelete; import org.hibernate.annotations.OnDeleteAction; + import org.jasig.cas.authentication.principal.Service; import org.jasig.cas.ticket.Ticket; import org.jasig.cas.ticket.TicketGrantingTicket; @@ -31,51 +32,54 @@ import javax.persistence.Entity; import javax.persistence.OneToOne; import javax.persistence.Table; + import java.util.Set; /** - * Refresh Token Implementation. + * The implementation class for {@link RefreshToken}. * * @author Michael Haselton - * @since 4.1.0 + * @author Longze Chen + * @since 4.1.5 */ @Entity @Table(name="REFRESHTOKEN") @Access(AccessType.FIELD) public final class RefreshTokenImpl extends AbstractToken implements RefreshToken { - /** Unique Id for serialization. */ + /** Unique id for serialization. */ private static final long serialVersionUID = -4808149803180911589L; - /** The TicketGrantingTicket this is associated with. */ + /** The ticket granting ticket this refresh token is associated with. */ @OneToOne(targetEntity=TicketGrantingTicketImpl.class, orphanRemoval=true) @OnDelete(action= OnDeleteAction.CASCADE) private TicketGrantingTicket ticketGrantingTicket; - /** The service associated with the tgt. */ + /** The service associated with the ticket granting ticket. */ @Column(name="SERVICE", nullable=false) private Service service; - /** - * Instantiates a new oauth refresh token impl. - */ - public RefreshTokenImpl() { - // nothing to do - } + /** Default constructor. */ + public RefreshTokenImpl(){} /** - * Constructs a new RefreshToken. + * Instantiate a new {@link RefreshTokenImpl}. * - * @param id the id of the Ticket + * @param id the id of the refresh token * @param clientId the client id * @param principalId the principal id * @param ticketGrantingTicket the ticket granting ticket * @param service the service * @param scopes the granted scopes */ - public RefreshTokenImpl(final String id, final String clientId, final String principalId, - final TicketGrantingTicket ticketGrantingTicket, final Service service, - final Set scopes) { + public RefreshTokenImpl( + final String id, + final String clientId, + final String principalId, + final TicketGrantingTicket ticketGrantingTicket, + final Service service, + final Set scopes + ) { super(id, clientId, principalId, TokenType.OFFLINE, scopes); this.ticketGrantingTicket = ticketGrantingTicket; this.service = service; diff --git a/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/token/Token.java b/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/token/Token.java index a6a5c229..ade73386 100644 --- a/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/token/Token.java +++ b/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/token/Token.java @@ -24,15 +24,16 @@ import java.util.Set; /** - * Interface for the generic concept of a token. + * The interface for the generic concept of a token. * * @author Michael Haselton - * @since 4.1.0 + * @author Longze Chen + * @since 4.1.5 */ public interface Token extends Serializable { /** - * Method to retrieve the id. + * Method to retrieve the id of the token. * * @return the id */ diff --git a/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/token/TokenType.java b/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/token/TokenType.java index 8c0349af..8a09512f 100644 --- a/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/token/TokenType.java +++ b/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/token/TokenType.java @@ -19,36 +19,68 @@ package org.jasig.cas.support.oauth.token; /** - * Token Type. + * Defines the enum data type for {@literal AbstractToken#type}. + * + * When talking about OAuth Token, we often refer to it in a way that is quite confusing. On one hand, from the + * perspective of OSF and CAS, there are four types conceptually: Access Token, Refresh Token, Personal Access Token + * and CAS Access Token. + * + * On the other hand, at the implementation level (defined in {@link TokenType}), the four types are OFFLINE - 0, + * ONLINE - 1, PERSONAL - 2 and CAS - 3. + * + * There are also three implementations of {@link Token}. {@link AccessTokenImpl} and {@link RefreshTokenImpl} are + * indeed OAuth Tokens while {@link AuthorizationCodeImpl} is actually OAuth Code. + * + * Here is a table that clarifies the confusion. + * + * | Conceptual-level Type | Implementation-level Type | Implementation Class | + * | ---------------------- | --------------------------| ----------------------------- | + * | Access Token | 0 or 1 | {@link AccessTokenImpl} | + * | Refresh Token | 0 | {@link RefreshTokenImpl} | + * | Personal Access Token | 2 | {@link AccessTokenImpl} | + * | CAS Access Token | 3 | {@link AccessTokenImpl} | + * | Authorization Code | 0 or 1 | {@link AuthorizationCodeImpl} | + * + * Note 1: {@link RefreshTokenImpl} is always of type 0 and {@link AccessTokenImpl} can be of any types from 0 to 3. + * + * Note 2: {@link org.jasig.cas.support.oauth.personal.PersonalAccessToken} is not the implementation for Personal + * Access Token. It is a helper class that temporarily stores the token information which is retrieved from the OSF + * database in {@literal io.cos.cas.adaptors.postgres.models.OpenScienceFrameworkApiOauth2PersonalAccessToken}. Then + * {@link AccessTokenImpl} uses the stored information to create the Personal Access Token. + * + * Note 3: Authorization Code is not an OAuth Token by concept. However, it does have a {@link Token} implementation + * {@link AuthorizationCodeImpl} with type 0 or 1. Its type determines 1) whether a Refresh Token is issued during the\ + * Token-Code exchange process and 2) the type of the Access Token issued. + * + * Note 4: CAS access token is only granted by + * {@link org.jasig.cas.support.oauth.CentralOAuthServiceImpl#grantCASAccessToken} during the CAS protocol 3.0 service + * validation process {@link org.jasig.cas.support.oauth.web.OAuth20ServiceValidateController}. Compared with ONLINE + * and OFFLINE access tokens, its behavior is slightly different. In addition, with current OSF settings, this access + * token is only stored in the user's OSF session. * * @author Michael Haselton - * @since 4.1.0 + * @author Longze Chen + * @since 4.1.5 */ public enum TokenType { - /** - * Offline Token. - */ + + /** Offline tokens. */ OFFLINE(0), - /** - * Online Token. - */ + + /** Online tokens. */ ONLINE(1), - /** - * Personal Token. - */ + + /** Personal access tokens. */ PERSONAL(2), - /** - * CAS Token. - */ + + /** CAS access tokens. */ CAS(3); - /** - * The value of the token enumeration. - */ + /** The value of the token enumeration. */ private final int value; /** - * Constructs a new Token Type. + * Constructs a new {@link TokenType}. * * @param newValue the value representing the token type. */ diff --git a/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/token/registry/JpaTokenRegistry.java b/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/token/registry/JpaTokenRegistry.java index c7a5960d..06cce555 100644 --- a/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/token/registry/JpaTokenRegistry.java +++ b/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/token/registry/JpaTokenRegistry.java @@ -26,27 +26,33 @@ import org.jasig.cas.support.oauth.token.RefreshTokenImpl; import org.jasig.cas.support.oauth.token.Token; import org.jasig.cas.support.oauth.token.TokenType; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; + import org.springframework.util.Assert; import javax.persistence.EntityManager; import javax.persistence.NoResultException; import javax.persistence.PersistenceContext; import javax.validation.constraints.NotNull; + import java.util.Collection; import java.util.HashSet; import java.util.Set; /** - * JPA Token Registry. + * JPA-based OAuth token registry. + * + * An implementation of {@link TokenRegistry} which uses Java Persistent API (JPA) to store access tokens. * * @author Michael Haselton - * @since 4.1.0 + * @author Longze Chen + * @since 4.1.5 */ public final class JpaTokenRegistry implements TokenRegistry { - /** The Commons Logging logger instance. */ + /** Log instance for logging events, info, warnings, errors, etc. */ protected final Logger logger = LoggerFactory.getLogger(getClass()); @NotNull @@ -67,6 +73,7 @@ public void updateToken(final Token token) { @Override public T getToken(final String tokenId, final Class clazz) throws ClassCastException { + Assert.notNull(clazz, "clazz cannot be null"); final T token = entityManager.find(getClassImplementation(clazz), tokenId); @@ -75,75 +82,93 @@ public T getToken(final String tokenId, final Class clazz) } if (!clazz.isAssignableFrom(token.getClass())) { - throw new ClassCastException("Token [" + token.getId() - + "] is of type " + token.getClass() - + " when we were expecting " + clazz); + throw new ClassCastException( + "Token [" + token.getId() + "] is of type " + token.getClass() + " when we were expecting " + clazz + ); } return token; } @Override - public Collection getClientTokens(final String clientId, final Class clazz) throws ClassCastException { + public Collection getClientTokens( + final String clientId, + final Class clazz + ) throws ClassCastException { + Assert.notNull(clientId, "clientId cannot be null"); Assert.notNull(clazz, "clazz cannot be null"); final Class clazzImpl = getClassImplementation(clazz); try { - return entityManager - .createQuery("select t from " + clazzImpl.getSimpleName() + " t where t.clientId = :clientId", clazzImpl) - .setParameter("clientId", clientId) - .getResultList(); + final String query = "select t from " + clazzImpl.getSimpleName() + " t where t.clientId = :clientId"; + return entityManager.createQuery(query, clazzImpl).setParameter("clientId", clientId).getResultList(); } catch (final NoResultException e) { return null; } } @Override - public Collection getClientPrincipalTokens(final String clientId, final String principalId, final Class clazz) - throws ClassCastException { + public Collection getClientPrincipalTokens( + final String clientId, + final String principalId, + final Class clazz + ) throws ClassCastException { return getClientPrincipalTokens(clientId, principalId, null, clazz); } @Override - public Collection getClientPrincipalTokens(final String clientId, final String principalId, final TokenType type, - final Class clazz) throws ClassCastException { + public Collection getClientPrincipalTokens( + final String clientId, + final String principalId, + final TokenType type, + final Class clazz + ) throws ClassCastException { + Assert.notNull(clientId, "clientId cannot be null"); Assert.notNull(principalId, "principalId cannot be null"); Assert.notNull(clazz, "clazz cannot be null"); + String query; final Class clazzImpl = getClassImplementation(clazz); try { if (type == null) { + query = "select t from " + clazzImpl.getSimpleName() + + " t where t.clientId = :clientId and t.principalId = :principalId"; return entityManager - .createQuery("select t from " + clazzImpl.getSimpleName() - + " t where t.clientId = :clientId and t.principalId = :principalId", clazzImpl) + .createQuery(query, clazzImpl) .setParameter("clientId", clientId) .setParameter("principalId", principalId) .getResultList(); + } else { + query = "select t from " + clazzImpl.getSimpleName() + + " t where t.clientId = :clientId and t.principalId = :principalId and t.type = :type"; + return entityManager + .createQuery(query, clazzImpl) + .setParameter("clientId", clientId) + .setParameter("principalId", principalId) + .setParameter("type", type) + .getResultList(); } - - return entityManager - .createQuery("select t from " + clazzImpl.getSimpleName() - + " t where t.clientId = :clientId and t.principalId = :principalId and t.type = :type", clazzImpl) - .setParameter("clientId", clientId) - .setParameter("principalId", principalId) - .setParameter("type", type) - .getResultList(); } catch (final NoResultException e) { return null; } } @Override - public Collection getPrincipalTokens(final String principalId, final Class clazz) throws ClassCastException { + public Collection getPrincipalTokens( + final String principalId, + final Class clazz + ) throws ClassCastException { + Assert.notNull(principalId, "principalId cannot be null"); Assert.notNull(clazz, "clazz cannot be null"); final Class clazzImpl = getClassImplementation(clazz); try { + final String query = "select t from " + clazzImpl.getSimpleName() + " t where t.principalId = :principalId"; return entityManager - .createQuery("select t from " + clazzImpl.getSimpleName() + " t where t.principalId = :principalId", clazzImpl) + .createQuery(query, clazzImpl) .setParameter("principalId", principalId) .getResultList(); } catch (final NoResultException e) { @@ -152,37 +177,46 @@ public Collection getPrincipalTokens(final String principal } @Override - public Boolean isToken(final String clientId, final String principalId, final Set scopes, - final Class clazz) { + public Boolean isToken( + final String clientId, + final String principalId, + final Set scopes, + final Class clazz + ) throws ClassCastException { return isToken(null, clientId, principalId, scopes, clazz); } @Override - public Boolean isToken(final TokenType type, final String clientId, final String principalId, - final Set scopes, final Class clazz) { + public Boolean isToken( + final TokenType type, + final String clientId, + final String principalId, + final Set scopes, + final Class clazz + ) throws ClassCastException { Assert.notNull(clientId, "clientId cannot be null"); Assert.notNull(principalId, "principalId cannot be null"); Assert.notNull(scopes, "scopes cannot be null"); Assert.notNull(clazz, "clazz cannot be null"); - final Class clazzImpl = getClassImplementation(clazz); + String query; final Collection tokens; + final Class clazzImpl = getClassImplementation(clazz); try { if (type == null) { + query = "select t from " + clazzImpl.getSimpleName() + " t where " + + "t.clientId = :clientId and t.principalId = :principalId and t.scopesHash = :scopesHash"; tokens = entityManager - .createQuery("select t from " + clazzImpl.getSimpleName() + " t " - + "where t.clientId = :clientId and t.principalId = :principalId and t.scopesHash = :scopesHash", - clazzImpl) + .createQuery(query, clazzImpl) .setParameter("clientId", clientId) .setParameter("principalId", principalId) .setParameter("scopesHash", scopes.hashCode()) .getResultList(); } else { + query = "select t from " + clazzImpl.getSimpleName() + " t where t.type = :type and " + + "t.clientId = :clientId and t.principalId = :principalId and t.scopesHash = :scopesHash"; tokens = entityManager - .createQuery("select t from " + clazzImpl.getSimpleName() + " t " - + "where t.type = :type and t.clientId = :clientId and t.principalId = :principalId " - + "and t.scopesHash = :scopesHash", - clazzImpl) + .createQuery(query, clazzImpl) .setParameter("type", type) .setParameter("clientId", clientId) .setParameter("principalId", principalId) @@ -204,23 +238,28 @@ public Boolean isToken(final TokenType type, final String clie @Override public Integer getPrincipalCount(final String clientId) { + Assert.notNull(clientId, "clientId cannot be null"); + String query; final Set principals = new HashSet<>(); + try { - principals.addAll(entityManager - .createQuery("select distinct t.principalId from RefreshTokenImpl t where t.clientId = :clientId", String.class) - .setParameter("clientId", clientId) - .getResultList()); + query = "select distinct t.principalId from " + + RefreshTokenImpl.class.getSimpleName() + " t where t.clientId = :clientId"; + principals.addAll( + entityManager.createQuery(query, String.class).setParameter("clientId", clientId).getResultList() + ); } catch (final NoResultException e) { // no results } try { - principals.addAll(entityManager - .createQuery("select distinct t.principalId from AccessTokenImpl t where t.clientId = :clientId", String.class) - .setParameter("clientId", clientId) - .getResultList()); + query = "select distinct t.principalId from " + + AccessTokenImpl.class.getSimpleName() + " t where t.clientId = :clientId"; + principals.addAll( + entityManager.createQuery(query, String.class).setParameter("clientId", clientId).getResultList() + ); } catch (final NoResultException e) { // no results } @@ -231,10 +270,10 @@ public Integer getPrincipalCount(final String clientId) { /** * Retrieve the token implementation class of the clazz specified. * - * @param clazz The assignable form of the implementation class. + * @param clazz the assignable form of the implementation class * @param the generic token type to return that extends {@link Token} - * @return the token implementation class. - * @throws ClassCastException the class cast exception. + * @return the token implementation class + * @throws ClassCastException the class cast exception */ @SuppressWarnings("unchecked") private Class getClassImplementation(final Class clazz) throws ClassCastException { @@ -245,8 +284,6 @@ private Class getClassImplementation(final Class clazz) } else if (AccessToken.class.isAssignableFrom(clazz)) { return (Class) AccessTokenImpl.class; } - - throw new ClassCastException("Could not cast " + clazz - + " to a suitable token implementation class"); + throw new ClassCastException("Could not cast " + clazz + " to a suitable token implementation class"); } } diff --git a/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/token/registry/TokenRegistry.java b/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/token/registry/TokenRegistry.java index 0064a293..e8fc7949 100644 --- a/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/token/registry/TokenRegistry.java +++ b/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/token/registry/TokenRegistry.java @@ -25,116 +25,140 @@ import java.util.Set; /** - * Token Registry interface. + * The interface defines what basic functionality a token registry should provide. * * @author Michael Haselton - * @since 4.1.0 + * @author Longze Chen + * @since 4.1.5 */ public interface TokenRegistry { /** - * Add a token to the registry. Token storage is based on the token id. + * Add a token to the registry. * - * @param token The token we wish to add to the cache. + * Token storage is based on the token id. + * + * @param token the token we wish to add to the cache */ void addToken(Token token); /** * Update a token in the registry. * - * @param token The token we wish to update in the cache. + * @param token the token we wish to update in the cache */ void updateToken(Token token); /** * Retrieve a token from the registry. * - * @param tokenId the id of the token we wish to retrieve. - * @param clazz The expected class of the token we wish to retrieve. + * @param tokenId the id of the token we wish to retrieve + * @param clazz the expected class of the token we wish to retrieve * @param the generic token type to return that extends {@link Token} - * @return the requested token. - * @throws ClassCastException the class cast exception. + * @return the requested token + * @throws ClassCastException the class cast exception */ T getToken(String tokenId, Class clazz) throws ClassCastException; /** * Retrieve a collection of tokens associated with the client id specified. * - * @param clientId the client id of the tokens we wish to retrieve. - * @param clazz The expected class of the token we wish to retrieve. + * @param clientId the client id of the tokens we wish to retrieve + * @param clazz the expected class of the token we wish to retrieve * @param the generic token type to return that extends {@link Token} - * @return a collection of requested tokens. - * @throws ClassCastException the class cast exception. + * @return a collection of requested tokens + * @throws ClassCastException the class cast exception */ Collection getClientTokens(String clientId, Class clazz) throws ClassCastException; /** - * Retrieve a collection of tokens associated with the client id & principal id specified. + * Retrieve a collection of tokens associated with the client id and the principal id specified. * - * @param clientId the client id of the tokens we wish to retrieve. - * @param principalId the principal id of the tokens we wish to retrieve. - * @param clazz The expected class of the token we wish to retrieve. + * @param clientId the client id of the tokens we wish to retrieve + * @param principalId the principal id of the tokens we wish to retrieve + * @param clazz the expected class of the token we wish to retrieve * @param the generic token type to return that extends {@link Token} - * @return a collection of requested tokens. - * @throws ClassCastException the class cast exception. + * @return a collection of requested tokens + * @throws ClassCastException the class cast exception */ - Collection getClientPrincipalTokens(String clientId, String principalId, Class clazz) throws ClassCastException; + Collection getClientPrincipalTokens( + String clientId, + String principalId, + Class clazz + ) throws ClassCastException; /** * Retrieve a collection of tokens associated with the client id, principal id and token type specified. * - * @param clientId the client id of the tokens we wish to retrieve. - * @param principalId the principal id of the tokens we wish to retrieve. - * @param type the token type of the tokens we wish to retrieve. - * @param clazz The expected class of the token we wish to retrieve. + * @param clientId the client id of the tokens we wish to retrieve + * @param principalId the principal id of the tokens we wish to retrieve + * @param type the token type of the tokens we wish to retrieve + * @param clazz the expected class of the token we wish to retrieve * @param the generic token type to return that extends {@link Token} - * @return a collection of requested tokens. - * @throws ClassCastException the class cast exception. + * @return a collection of requested tokens + * @throws ClassCastException the class cast exception */ - Collection getClientPrincipalTokens(String clientId, String principalId, TokenType type, Class clazz) - throws ClassCastException; + Collection getClientPrincipalTokens( + String clientId, + String principalId, + TokenType type, + Class clazz + ) throws ClassCastException; /** * Retrieve a collection of tokens associated with the principal id specified. * - * @param principalId the principal id of the tokens we wish to retrieve. - * @param clazz The expected class of the token we wish to retrieve. + * @param principalId the principal id of the tokens we wish to retrieve + * @param clazz the expected class of the token we wish to retrieve * @param the generic token type to return that extends {@link Token} - * @return a collection of requested tokens. - * @throws ClassCastException the class cast exception. + * @return a collection of requested tokens + * @throws ClassCastException the class cast exception */ Collection getPrincipalTokens(String principalId, Class clazz) throws ClassCastException; /** * Check if a token exists by client id, principal id and assigned scopes. * - * @param clientId the client id of the token we wish to find. - * @param principalId the principal id of the token we wish to find. - * @param scopes the scopes associated with the token we wish to find. - * @param clazz The expected class of the token we wish to find. + * @param clientId the client id of the token we wish to find + * @param principalId the principal id of the token we wish to find + * @param scopes the scopes associated with the token we wish to find + * @param clazz the expected class of the token we wish to find * @param the generic token type to return that extends {@link Token} - * @return indicates if the token could be found. + * @return indicates if the token could be found + * @throws ClassCastException the class cast exception */ - Boolean isToken(String clientId, String principalId, Set scopes, Class clazz); + Boolean isToken( + String clientId, + String principalId, + Set scopes, + Class clazz + ) throws ClassCastException; /** * Check if a token exists by token type, client id, principal id and assigned scopes. * - * @param type the token type of the token we wish to find. - * @param clientId the client id of the token we wish to find. - * @param principalId the principal id of the token we wish to find. - * @param scopes the scopes associated with the token we wish to find. - * @param clazz The expected class of the token we wish to find. + * @param type the token type of the token we wish to find + * @param clientId the client id of the token we wish to find + * @param principalId the principal id of the token we wish to find + * @param scopes the scopes associated with the token we wish to find + * @param clazz the expected class of the token we wish to find * @param the generic token type to return that extends {@link Token} - * @return indicates if the token could be found. + * @return indicates if the token could be found + * @throws ClassCastException the class cast exception */ - Boolean isToken(TokenType type, String clientId, String principalId, Set scopes, Class clazz); + Boolean isToken( + TokenType type, + String clientId, + String principalId, + Set scopes, + Class clazz + ) throws ClassCastException; /** * Count the number unique principal's assigned to a client token id. * - * @param clientId the client id of the tokens we wish to find. - * @return a count of the number of unique principals. + * @param clientId the client id of the tokens we wish to find + * @return a count of the number of unique principals */ Integer getPrincipalCount(String clientId); } diff --git a/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/web/OAuth20AuthorizeCallbackActionController.java b/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/web/OAuth20AuthorizeCallbackActionController.java index a9f17801..0df3a4b4 100644 --- a/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/web/OAuth20AuthorizeCallbackActionController.java +++ b/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/web/OAuth20AuthorizeCallbackActionController.java @@ -19,6 +19,7 @@ package org.jasig.cas.support.oauth.web; import org.apache.commons.lang3.StringUtils; + import org.jasig.cas.support.oauth.CentralOAuthService; import org.jasig.cas.support.oauth.InvalidParameterException; import org.jasig.cas.support.oauth.OAuthConstants; @@ -26,49 +27,67 @@ import org.jasig.cas.support.oauth.token.AccessToken; import org.jasig.cas.support.oauth.token.AuthorizationCode; import org.jasig.cas.support.oauth.token.TokenType; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; + import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.mvc.AbstractController; +import java.util.Set; +import java.util.concurrent.TimeUnit; + import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; -import java.util.Set; -import java.util.concurrent.TimeUnit; /** - * This controller is called if a user selects an action to allow or deny - * authorization. + * The OAuth 2.0 authorization callback action controller. + * + * This controller is called 1) if the user clicks an action button on the OAuth confirmation page to allow / deny the + * authorization or 2) if the {@link OAuth20AuthorizeCallbackController} decides to bypass the aforementioned explicit + * user confirmation with "allow" as the action. If allowed, the user is redirected to the "redirect uri" with `token` + * or `code` and other parameters appended as fragment (after the "#"). Otherwise if denied, the user is redirected to + * the "redirect uri" with the query parameter `error=access_denied`. * * @author Michael Haselton - * @since 4.1.0 + * @author Longze Chen + * @since 4.1.5 */ public final class OAuth20AuthorizeCallbackActionController extends AbstractController { + /** Log instance for logging events, info, warnings, errors, etc. */ private static final Logger LOGGER = LoggerFactory.getLogger(OAuth20AuthorizeCallbackActionController.class); + /** The CAS OAuth authorization service. */ private final CentralOAuthService centralOAuthService; + /** The ticket timeout. */ private final Long timeout; /** - * Instantiates a new o auth20 authorize callback action controller. + * Instantiates a new {@link OAuth20AuthorizeCallbackActionController}. * * @param centralOAuthService the central oauth service * @param timeout the ticket timeout */ - public OAuth20AuthorizeCallbackActionController(final CentralOAuthService centralOAuthService, final Long timeout) { + public OAuth20AuthorizeCallbackActionController( + final CentralOAuthService centralOAuthService, + final Long timeout + ) { this.centralOAuthService = centralOAuthService; this.timeout = timeout; } @Override - protected ModelAndView handleRequestInternal(final HttpServletRequest request, final HttpServletResponse response) - throws Exception { + protected ModelAndView handleRequestInternal( + final HttpServletRequest request, + final HttpServletResponse response + ) throws Exception { + final HttpSession session = request.getSession(); - // get action + // Retrieve the authorization action of the the user. final String action = request.getParameter(OAuthConstants.OAUTH20_APPROVAL_PROMPT_ACTION); LOGGER.debug("{} : {}", OAuthConstants.OAUTH20_APPROVAL_PROMPT_ACTION, action); @@ -80,7 +99,7 @@ protected ModelAndView handleRequestInternal(final HttpServletRequest request, f LOGGER.debug("{} : {}", OAuthConstants.OAUTH20_CLIENT_ID, clientId); session.removeAttribute(OAuthConstants.OAUTH20_CLIENT_ID); - // retrieve state from session (csrf equivalent) + // Retrieve the state from session, which is equivalent to using CSRF token and prevents CSRF. final String state = (String) session.getAttribute(OAuthConstants.OAUTH20_STATE); LOGGER.debug("{} : {}", OAuthConstants.OAUTH20_STATE, state); session.removeAttribute(OAuthConstants.OAUTH20_STATE); @@ -102,6 +121,7 @@ protected ModelAndView handleRequestInternal(final HttpServletRequest request, f LOGGER.debug("{} : {}", OAuthConstants.OAUTH20_SCOPE_SET, scopeSet); session.removeAttribute(OAuthConstants.OAUTH20_SCOPE_SET); + // The user has denied the authorization. Redirect to the "redirect uri" with an error query parameter. if (!action.equalsIgnoreCase(OAuthConstants.OAUTH20_APPROVAL_PROMPT_ACTION_ALLOW)) { LOGGER.warn("Approval Prompt Action was denied by the user."); final String deniedCallbackUrl = OAuthUtils.addParameter(redirectUri, OAuthConstants.ERROR, OAuthConstants.ACCESS_DENIED); @@ -119,16 +139,18 @@ protected ModelAndView handleRequestInternal(final HttpServletRequest request, f throw new InvalidParameterException(OAuthConstants.OAUTH20_REDIRECT_URI); } + // TODO: The "token" response type should be disabled. It is very dangerous to include the access token in the + // URL as a query parameter (as opposed to be returned in a HTTPS POST response body) since it is neither + // one-time or short-lived (as opposed to the authorization code). if ("token".equals(responseType)) { - final AuthorizationCode authorizationCode = centralOAuthService.grantAuthorizationCode( - TokenType.ONLINE, clientId, loginTicketId, redirectUri, scopeSet); + final AuthorizationCode authorizationCode = centralOAuthService + .grantAuthorizationCode(TokenType.ONLINE, clientId, loginTicketId, redirectUri, scopeSet); final AccessToken accessToken = centralOAuthService.grantOnlineAccessToken(authorizationCode); - String callbackUrl = redirectUri; + final long timeSinceTicketCreation = System.currentTimeMillis() - accessToken.getTicket().getCreationTime(); + final int expiresIn = (int) (timeout - TimeUnit.MILLISECONDS.toSeconds(timeSinceTicketCreation)); callbackUrl += "#" + OAuthConstants.ACCESS_TOKEN + "=" + accessToken.getId(); - callbackUrl += "&" + OAuthConstants.EXPIRES_IN + "=" - + (int) (timeout - TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis() - - accessToken.getTicket().getCreationTime())); + callbackUrl += "&" + OAuthConstants.EXPIRES_IN + "=" + expiresIn; callbackUrl += "&" + OAuthConstants.TOKEN_TYPE + "=" + OAuthConstants.BEARER_TOKEN; if (!StringUtils.isBlank(state)) { callbackUrl += "&" + OAuthConstants.STATE + "=" + state; @@ -137,10 +159,10 @@ protected ModelAndView handleRequestInternal(final HttpServletRequest request, f return OAuthUtils.redirectTo(callbackUrl); } - // response type is code - final AuthorizationCode authorizationCode = centralOAuthService.grantAuthorizationCode( - tokenType, clientId, loginTicketId, redirectUri, scopeSet); - + // Response type is "code", redirect to the callback url (which is the redirect uri of the registered service + // instead of the OAuth authorization callback endpoints this time) with code and state. + final AuthorizationCode authorizationCode = centralOAuthService + .grantAuthorizationCode(tokenType, clientId, loginTicketId, redirectUri, scopeSet); String callbackUrl = OAuthUtils.addParameter(redirectUri, OAuthConstants.CODE, authorizationCode.getId()); if (!StringUtils.isBlank(state)) { callbackUrl = OAuthUtils.addParameter(callbackUrl, OAuthConstants.STATE, state); diff --git a/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/web/OAuth20AuthorizeCallbackController.java b/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/web/OAuth20AuthorizeCallbackController.java index 35434aef..01ab3db7 100644 --- a/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/web/OAuth20AuthorizeCallbackController.java +++ b/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/web/OAuth20AuthorizeCallbackController.java @@ -20,104 +20,145 @@ import org.apache.commons.lang3.StringUtils; import org.apache.http.HttpStatus; + import org.jasig.cas.support.oauth.CentralOAuthService; import org.jasig.cas.support.oauth.OAuthConstants; import org.jasig.cas.support.oauth.OAuthUtils; import org.jasig.cas.support.oauth.scope.Scope; import org.jasig.cas.support.oauth.token.TokenType; + import org.jasig.cas.ticket.ServiceTicket; import org.jasig.cas.ticket.TicketGrantingTicket; import org.jasig.cas.ticket.registry.TicketRegistry; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; + import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.mvc.AbstractController; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import javax.servlet.http.HttpSession; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; + /** - * This controller is called after successful authentication and - * redirects user to the callback url of the OAuth application. A code is - * added which is the service ticket retrieved from previous authentication. + * The OAuth 2.0 authorization callback controller. + * + * This controller is called twice. In the first pass, it handles one step of the CAS protocol; in the second one, it + * handles one step of the OAuth protocol. + * + * It is first called after the first authentication step of the CAS protocol after credentials check has passed and a + * service ticket pending validation has been issued. In this pass, it handles the second step of the CAS protocol: + * validating the service ticket and granting a ticket granting ticket. One important and interesting thing to note is + * that this controller acts smartly as both a CAS client and a CAS server in the process. This saves the extra efforts + * for talking to the primary CAS service as well as preserving session. + * + * After successful CAS authentication (both steps done, service ticket removed and ticket granting ticket generated), + * the controller redirects back to itself to be called a second time. Depending on settings of the registered service + * and OAuth parameters, it either redirects the user to the callback url for allowing the authorization or the view + * for asking user to confirm the authorization action. * * @author Jerome Leleu * @author Michael Haselton - * @since 3.5.0 + * @author Longze Chen + * @since 4.1.5 */ public final class OAuth20AuthorizeCallbackController extends AbstractController { + /** Log instance for logging events, info, warnings, errors, etc. */ private static final Logger LOGGER = LoggerFactory.getLogger(OAuth20AuthorizeCallbackController.class); + /** The CAS OAuth authorization service. */ private final CentralOAuthService centralOAuthService; + /** The ticket registry for accessing (retrieving and deleting) tickets. */ private final TicketRegistry ticketRegistry; /** - * Instantiates a new o auth20 authorize callback controller. + * Instantiates a new {@link OAuth20AuthorizeCallbackController}. * - * @param centralOAuthService the central oauth service + * @param centralOAuthService the the CAS OAuth service * @param ticketRegistry the ticket registry */ - public OAuth20AuthorizeCallbackController(final CentralOAuthService centralOAuthService, final TicketRegistry ticketRegistry) { + public OAuth20AuthorizeCallbackController( + final CentralOAuthService centralOAuthService, + final TicketRegistry ticketRegistry + ) { this.centralOAuthService = centralOAuthService; this.ticketRegistry = ticketRegistry; } @Override - protected ModelAndView handleRequestInternal(final HttpServletRequest request, final HttpServletResponse response) - throws Exception { + protected ModelAndView handleRequestInternal( + final HttpServletRequest request, + final HttpServletResponse response + ) throws Exception { + + // Retrieve the session which stores the current authorization parameters. final HttpSession session = request.getSession(); - // get cas login service ticket + // Before first pass: the login service ticket is a query parameter the first time this controller is called. + // There is no ticket granting ticket in the user session. Before second pass: the login service ticket has + // been removed and a ticket granting ticket has been generated and stored in the user session. + + // Retrieve the login service ticket and determine which pass the controller is called. final String serviceTicketId = request.getParameter(OAuthConstants.TICKET); LOGGER.debug("{} : {}", OAuthConstants.TICKET, serviceTicketId); - // first time this url is requested the login ticket will be a query parameter + // Service ticket found in the query parameters of the request, first pass starts. if (serviceTicketId != null) { - // create the login ticket granting ticket + + // Create the login ticket granting ticket from the service ticket final ServiceTicket serviceTicket = (ServiceTicket) ticketRegistry.getTicket(serviceTicketId); if (serviceTicket == null || serviceTicket.isExpired()) { LOGGER.error("Service Ticket expired : {}", serviceTicketId); - return OAuthUtils.writeJsonError(response, OAuthConstants.INVALID_GRANT, OAuthConstants.EXPIRED_ST_DESCRIPTION, - HttpStatus.SC_BAD_REQUEST); + return OAuthUtils.writeJsonError( + response, + OAuthConstants.INVALID_GRANT, + OAuthConstants.EXPIRED_ST_DESCRIPTION, + HttpStatus.SC_BAD_REQUEST + ); } - final TicketGrantingTicket ticketGrantingTicket = serviceTicket.getGrantingTicket(); - // remove login service ticket + // Remove login service ticket. ticketRegistry.deleteTicket(serviceTicket.getId()); - // store the login tgt id in the user's session, used to create service tickets for validation and - // oauth credentials later in the flow + // Store the login ticket granting ticket id in the OAuth session. session.setAttribute(OAuthConstants.OAUTH20_LOGIN_TICKET_ID, ticketGrantingTicket.getId()); - // redirect back to self, clears the service ticket from the url, allows the page to be refreshed w/o error + // Redirects back to itself to start the second pass. return OAuthUtils.redirectTo(request.getRequestURL().toString()); } - // get cas login service ticket from the session + // No service ticket is found, second pass starts. + + // Retrieve the login ticket granting ticket. final String ticketGrantingTicketId = (String) session.getAttribute(OAuthConstants.OAUTH20_LOGIN_TICKET_ID); LOGGER.debug("{} : {}", OAuthConstants.TICKET, ticketGrantingTicketId); - // verify the login ticket granting ticket is still valid - final TicketGrantingTicket ticketGrantingTicket = (TicketGrantingTicket) ticketRegistry.getTicket(ticketGrantingTicketId); + // Verify the login ticket granting ticket is still valid. + final TicketGrantingTicket ticketGrantingTicket + = (TicketGrantingTicket) ticketRegistry.getTicket(ticketGrantingTicketId); if (ticketGrantingTicket == null || ticketGrantingTicket.isExpired()) { LOGGER.error("Ticket Granting Ticket expired : {}", ticketGrantingTicketId); - // display error view as we are still interacting w/ the user. + // Display the error view as we are still interacting w/ the user. return OAuthUtils.writeJsonError(response, OAuthConstants.INVALID_GRANT, OAuthConstants.EXPIRED_TGT_DESCRIPTION, HttpStatus.SC_BAD_REQUEST); } - final String callbackUrl = request.getRequestURL().toString() - .replace("/" + OAuthConstants.CALLBACK_AUTHORIZE_URL, "/" + OAuthConstants.CALLBACK_AUTHORIZE_ACTION_URL); + // Build the callback url for allowing the authorization. + final String callbackUrl = request.getRequestURL().toString().replace( + "/" + OAuthConstants.CALLBACK_AUTHORIZE_URL, + "/" + OAuthConstants.CALLBACK_AUTHORIZE_ACTION_URL + ); LOGGER.debug("{} : {}", OAuthConstants.CALLBACK_AUTHORIZE_ACTION_URL, callbackUrl); final String clientId = (String) session.getAttribute(OAuthConstants.OAUTH20_CLIENT_ID); @@ -133,26 +174,35 @@ protected ModelAndView handleRequestInternal(final HttpServletRequest request, f LOGGER.debug("{} : {}", OAuthConstants.OAUTH20_TOKEN_TYPE, tokenType); final String approvalPrompt = (String) session.getAttribute(OAuthConstants.OAUTH20_APPROVAL_PROMPT); - LOGGER.debug("{} : {}", OAuthConstants.OAUTH20_APPROVAL_PROMPT); + LOGGER.debug("{} : {}", OAuthConstants.OAUTH20_APPROVAL_PROMPT, approvalPrompt); final Boolean bypassApprovalPrompt = (Boolean) session.getAttribute(OAuthConstants.BYPASS_APPROVAL_PROMPT); LOGGER.debug("{} : {}", OAuthConstants.BYPASS_APPROVAL_PROMPT, bypassApprovalPrompt); final Set requestedScopeSet = new HashSet<>(Arrays.asList(scope.split(" "))); - // we use the scope map rather than scope set as the oauth service has the potential to add default scopes(s). + // Use a map rather a set for scopes as the OAuth service has the potential to add default scopes(s). final Map scopeMap = centralOAuthService.getScopes(requestedScopeSet); session.setAttribute(OAuthConstants.OAUTH20_SCOPE_SET, new HashSet<>(scopeMap.keySet())); - final String allowCallbackUrl = OAuthUtils.addParameter(callbackUrl, OAuthConstants.OAUTH20_APPROVAL_PROMPT_ACTION, - OAuthConstants.OAUTH20_APPROVAL_PROMPT_ACTION_ALLOW); - // this override can only be set on the service itself + // Update the callback url with action "allow". + final String allowCallbackUrl = OAuthUtils.addParameter( + callbackUrl, + OAuthConstants.OAUTH20_APPROVAL_PROMPT_ACTION, + OAuthConstants.OAUTH20_APPROVAL_PROMPT_ACTION_ALLOW + ); + + // Final redirect - option 1: Ignore the `OAUTH20_APPROVAL_PROMPT` parameter in session and redirect to the + // callback URL for allowing the authorization if the `BYPASS_APPROVAL_PROMPT` parameter is set. This override + // can ONLY be set by the OAuth registered service itself. if (bypassApprovalPrompt != null && bypassApprovalPrompt) { return OAuthUtils.redirectTo(allowCallbackUrl); } - // if approval prompt is not forced, check if we have already approved the requested scopes, - // if so do not ask the user again for authorization. - if (StringUtils.isBlank(approvalPrompt) || !approvalPrompt.equalsIgnoreCase(OAuthConstants.APPROVAL_PROMPT_FORCE)) { + + // Final redirect - option 2: If `OAUTH20_APPROVAL_PROMPT` is automatic, check if we have already approved the + // requested scopes, if so do not ask the user again for authorization. + if (StringUtils.isBlank(approvalPrompt) + || !approvalPrompt.equalsIgnoreCase(OAuthConstants.APPROVAL_PROMPT_FORCE)) { final String principalId = ticketGrantingTicket.getAuthentication().getPrincipal().getId(); final Boolean existingToken = (tokenType == TokenType.ONLINE) ? centralOAuthService.isAccessToken(tokenType, clientId, principalId, scopeMap.keySet()) @@ -163,6 +213,8 @@ protected ModelAndView handleRequestInternal(final HttpServletRequest request, f } } + // Final redirect - option 3: If `OAUTH20_APPROVAL_PROMPT` is forced or if we haven't previously approved the + // requested scopes, redirect to the authorization confirmation page for the user to approve the action. final Map model = new HashMap<>(); model.put("callbackUrl", callbackUrl); model.put("scopeMap", scopeMap); diff --git a/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/web/OAuth20AuthorizeController.java b/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/web/OAuth20AuthorizeController.java index 8ada4b09..39b29c7c 100644 --- a/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/web/OAuth20AuthorizeController.java +++ b/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/web/OAuth20AuthorizeController.java @@ -19,14 +19,17 @@ package org.jasig.cas.support.oauth.web; import org.apache.commons.lang3.StringUtils; + import org.jasig.cas.support.oauth.CentralOAuthService; import org.jasig.cas.support.oauth.InvalidParameterException; import org.jasig.cas.support.oauth.OAuthConstants; import org.jasig.cas.support.oauth.OAuthUtils; import org.jasig.cas.support.oauth.services.OAuthRegisteredService; import org.jasig.cas.support.oauth.token.TokenType; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; + import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.mvc.AbstractController; @@ -35,9 +38,12 @@ import javax.servlet.http.HttpSession; /** - * This controller is in charge of responding to the authorize call in - * OAuth protocol. It stores the callback url and redirects user to the - * login page with the callback service. + * The OAuth 2.0 authorization controller. + * + * This controller handles the client's the initial authorization request {@literal /oauth2/authorize}. It verifies the + * request, stores the authorization parameters in session and finally redirects the user to the CAS default login page + * for primary authentication with the authorization callback endpoint {@literal /oauth2/callbackAuthorize} as service, + * which further handles CAS service validation and OAuth authorization callback. * * @author Jerome Leleu * @author Michael Haselton @@ -46,17 +52,20 @@ */ public final class OAuth20AuthorizeController extends AbstractController { + /** Log instance for logging events, info, warnings, errors, etc. */ private static final Logger LOGGER = LoggerFactory.getLogger(OAuth20AuthorizeController.class); + /** The CAS OAuth authorization service. */ private final CentralOAuthService centralOAuthService; + /** The primary CAS authentication login url. */ private final String loginUrl; /** - * Instantiates a new o auth20 authorize controller. + * Instantiates a new {@link OAuth20AuthorizeController}. * - * @param centralOAuthService the central oauth service - * @param loginUrl the login url + * @param centralOAuthService the CAS OAuth service + * @param loginUrl the CAS login url */ public OAuth20AuthorizeController(final CentralOAuthService centralOAuthService, final String loginUrl) { this.centralOAuthService = centralOAuthService; @@ -64,8 +73,11 @@ public OAuth20AuthorizeController(final CentralOAuthService centralOAuthService, } @Override - protected ModelAndView handleRequestInternal(final HttpServletRequest request, final HttpServletResponse response) - throws Exception { + protected ModelAndView handleRequestInternal( + final HttpServletRequest request, + final HttpServletResponse response + ) throws Exception { + final String responseType = request.getParameter(OAuthConstants.RESPONSE_TYPE); LOGGER.debug("{} : {}", OAuthConstants.RESPONSE_TYPE, responseType); @@ -87,45 +99,70 @@ protected ModelAndView handleRequestInternal(final HttpServletRequest request, f final String approvalPrompt = request.getParameter(OAuthConstants.APPROVAL_PROMPT); LOGGER.debug("{} : {}", OAuthConstants.APPROVAL_PROMPT, approvalPrompt); + // Verify the authorization request. verifyRequest(responseType, clientId, redirectUri, accessType); + // Retrieve the OAuth registered service. final OAuthRegisteredService service = centralOAuthService.getRegisteredService(clientId); if (service == null) { LOGGER.error("Unknown {} : {}", OAuthConstants.CLIENT_ID, clientId); throw new InvalidParameterException(OAuthConstants.CLIENT_ID); } - // Redirect URI is a literal string (not regex) and the match is done by case-insensitive equality check. + + // Verify the redirect uri, which is stored as the `serviceId` property of the `OAuthRegisteredService` class. + // It is not a regular expression but a literal string and thus the match is done by a case-insensitive string + // equality check. if (!redirectUri.equalsIgnoreCase(service.getServiceId())) { - LOGGER.error("Unmatched {} : {} for serviceId : {}", OAuthConstants.REDIRECT_URI, redirectUri, service.getServiceId()); + LOGGER.error( + "Unmatched {} : {} for serviceId : {}", + OAuthConstants.REDIRECT_URI, + redirectUri, service.getServiceId() + ); throw new InvalidParameterException(OAuthConstants.REDIRECT_URI); } - // keep info in session + // Keep the authorization parameters in session. final HttpSession session = request.getSession(); session.setAttribute(OAuthConstants.BYPASS_APPROVAL_PROMPT, service.isBypassApprovalPrompt()); - session.setAttribute(OAuthConstants.OAUTH20_APPROVAL_PROMPT, StringUtils.isBlank(approvalPrompt) - ? OAuthConstants.APPROVAL_PROMPT_AUTO : approvalPrompt); - session.setAttribute(OAuthConstants.OAUTH20_TOKEN_TYPE, TokenType.valueOf(StringUtils.isBlank(accessType) - ? "ONLINE" : accessType.toUpperCase())); - session.setAttribute(OAuthConstants.OAUTH20_RESPONSE_TYPE, StringUtils.isBlank(responseType) ? "code" : responseType.toLowerCase()); + session.setAttribute( + OAuthConstants.OAUTH20_APPROVAL_PROMPT, + StringUtils.isBlank(approvalPrompt) ? OAuthConstants.APPROVAL_PROMPT_AUTO : approvalPrompt + ); + session.setAttribute( + OAuthConstants.OAUTH20_TOKEN_TYPE, + TokenType.valueOf(StringUtils.isBlank(accessType) ? "ONLINE" : accessType.toUpperCase()) + ); + session.setAttribute( + OAuthConstants.OAUTH20_RESPONSE_TYPE, + StringUtils.isBlank(responseType) ? "code" : responseType.toLowerCase() + ); session.setAttribute(OAuthConstants.OAUTH20_CLIENT_ID, clientId); session.setAttribute(OAuthConstants.OAUTH20_REDIRECT_URI, redirectUri); session.setAttribute(OAuthConstants.OAUTH20_SERVICE_NAME, service.getName()); - session.setAttribute(OAuthConstants.OAUTH20_SCOPE, StringUtils.isBlank(scope) ? "" : scope); + session.setAttribute( + OAuthConstants.OAUTH20_SCOPE, + StringUtils.isBlank(scope) ? "" : scope + ); session.setAttribute(OAuthConstants.OAUTH20_STATE, state); - final String callbackAuthorizeUrl = request.getRequestURL().toString() - .replace("/" + OAuthConstants.AUTHORIZE_URL, "/" + OAuthConstants.CALLBACK_AUTHORIZE_URL); + // Generate the authorization callback url. + final String callbackAuthorizeUrl = request.getRequestURL().toString().replace( + "/" + OAuthConstants.AUTHORIZE_URL, + "/" + OAuthConstants.CALLBACK_AUTHORIZE_URL + ); LOGGER.debug("{} : {}", OAuthConstants.CALLBACK_AUTHORIZE_URL, callbackAuthorizeUrl); - final String loginUrlWithService = OAuthUtils.addParameter(loginUrl, OAuthConstants.SERVICE, callbackAuthorizeUrl); + // Generate the CAS login url with the authorization callback url as service. + final String loginUrlWithService + = OAuthUtils.addParameter(loginUrl, OAuthConstants.SERVICE, callbackAuthorizeUrl); LOGGER.debug("loginUrlWithService : {}", loginUrlWithService); + // Finally, redirect to the CAS default login endpoint. return OAuthUtils.redirectTo(loginUrlWithService); } /** - * Verify the request by reviewing the values of client id, redirect uri, etc... + * Verify the request by reviewing the values of OAuth 2.0 parameters. * * @param responseType the response type * @param clientId the client id @@ -133,26 +170,34 @@ protected ModelAndView handleRequestInternal(final HttpServletRequest request, f * @param accessType the access type * @throws InvalidParameterException with the name of the invalid parameter */ - private void verifyRequest(final String responseType, final String clientId, final String redirectUri, final String accessType) - throws InvalidParameterException { - // responseType must be valid + private void verifyRequest( + final String responseType, + final String clientId, + final String redirectUri, + final String accessType + ) throws InvalidParameterException { + + // Response type can either be "code" or "token", default (if not provided) is "code". if (!StringUtils.isBlank(responseType)) { if (!"code".equalsIgnoreCase(responseType) && !"token".equalsIgnoreCase(responseType)) { LOGGER.error("Invalid {} specified : {}", OAuthConstants.RESPONSE_TYPE, responseType); throw new InvalidParameterException(OAuthConstants.RESPONSE_TYPE); } } - // clientId is required + + // Client id is required (not empty). if (StringUtils.isBlank(clientId)) { LOGGER.error("Missing {}", OAuthConstants.CLIENT_ID); throw new InvalidParameterException(OAuthConstants.CLIENT_ID); } - // redirectUri is required + + // Redirect uri is required (not empty). if (StringUtils.isBlank(redirectUri)) { LOGGER.error("Missing {}", OAuthConstants.REDIRECT_URI); throw new InvalidParameterException(OAuthConstants.REDIRECT_URI); } - // accessType must be valid, default is ONLINE + + // Access type can either be "OFFLINE" or "ONLINE", default (if not provided) is "ONLINE". if (!StringUtils.isBlank(accessType)) { try { final TokenType tokenType = TokenType.valueOf(accessType.toUpperCase()); diff --git a/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/web/OAuth20MetadataClientController.java b/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/web/OAuth20MetadataClientController.java index d0c68a38..2f4e4dd7 100644 --- a/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/web/OAuth20MetadataClientController.java +++ b/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/web/OAuth20MetadataClientController.java @@ -19,103 +19,118 @@ package org.jasig.cas.support.oauth.web; import com.fasterxml.jackson.databind.ObjectMapper; + import org.apache.commons.lang3.StringUtils; import org.apache.http.HttpStatus; + import org.jasig.cas.support.oauth.CentralOAuthService; import org.jasig.cas.support.oauth.InvalidParameterException; import org.jasig.cas.support.oauth.OAuthConstants; import org.jasig.cas.support.oauth.OAuthUtils; import org.jasig.cas.support.oauth.metadata.ClientMetadata; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; + import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.mvc.AbstractController; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; import java.util.HashMap; import java.util.Map; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + /** - * This controller handles requests for metadata regarding a client. + * The OAuth 2.0 "Metadata Client" Controller. + * + * This controller handles requests that ask for the metadata regarding an OAuth registered service. The service is + * identified and retrieved by the given client id while the access check (authentication) is verified using the given + * client secret. * * @author Michael Haselton - * @since 4.1.0 + * @author Longze Chen + * @since 4.1.5 */ public final class OAuth20MetadataClientController extends AbstractController { + /** Log instance for logging events, info, warnings, errors, etc. */ private static final Logger LOGGER = LoggerFactory.getLogger(OAuth20MetadataClientController.class); - private static final String CLIENT_ID = "client_id"; - - private static final String NAME = "name"; - - private static final String DESCRIPTION = "description"; - - private static final String USERS = "users"; - + /** The CAS OAuth authorization service. */ private final CentralOAuthService centralOAuthService; /** - * Instantiates a new o auth20 client metadata controller. + * Instantiates a new {@link OAuth20MetadataClientController}. * - * @param centralOAuthService the central oauth service + * @param centralOAuthService the CAS OAuth service */ public OAuth20MetadataClientController(final CentralOAuthService centralOAuthService) { this.centralOAuthService = centralOAuthService; } @Override - protected ModelAndView handleRequestInternal(final HttpServletRequest request, final HttpServletResponse response) - throws Exception { + protected ModelAndView handleRequestInternal( + final HttpServletRequest request, + final HttpServletResponse response + ) throws Exception { + + // Verify that all required OAuth 2.0 parameters are provided. final String clientId = request.getParameter(OAuthConstants.CLIENT_ID); LOGGER.debug("{} : {}", OAuthConstants.CLIENT_ID, clientId); - final String clientSecret = request.getParameter(OAuthConstants.CLIENT_SECRET); LOGGER.debug("{} : {}", OAuthConstants.CLIENT_SECRET, "************"); - try { verifyRequest(clientId, clientSecret); } catch (final InvalidParameterException e) { - return OAuthUtils.writeJsonError(response, OAuthConstants.INVALID_REQUEST, e.getMessage(), HttpStatus.SC_BAD_REQUEST); + return OAuthUtils.writeJsonError( + response, + OAuthConstants.INVALID_REQUEST, + e.getMessage(), + HttpStatus.SC_BAD_REQUEST + ); } + // Retrieve metadata about the OAuth registered service using the given client id and secret. final ClientMetadata metadata = centralOAuthService.getClientMetadata(clientId, clientSecret); if (metadata == null) { LOGGER.debug("Metadata could not be retrieved for the Client ID and Client Secret specified"); - return OAuthUtils.writeJsonError(response, OAuthConstants.INVALID_REQUEST, - OAuthConstants.INVALID_CLIENT_ID_OR_SECRET_DESCRIPTION, HttpStatus.SC_BAD_REQUEST); + return OAuthUtils.writeJsonError( + response, + OAuthConstants.INVALID_REQUEST, + OAuthConstants.INVALID_CLIENT_ID_OR_SECRET_DESCRIPTION, + HttpStatus.SC_BAD_REQUEST + ); } + // Build and return the response. final Map map = new HashMap<>(); - map.put(CLIENT_ID, metadata.getClientId()); - map.put(NAME, metadata.getName()); - map.put(DESCRIPTION, metadata.getDescription()); - map.put(USERS, metadata.getUsers()); - + map.put(OAuthConstants.CLIENT_ID, metadata.getClientId()); + map.put(OAuthConstants.SERVICE_NAME, metadata.getName()); + map.put(OAuthConstants.SERVICE_DESCRIPTION, metadata.getDescription()); + map.put(OAuthConstants.SERVICE_USERS, metadata.getUsers()); final String result = new ObjectMapper().writeValueAsString(map); LOGGER.debug("result : {}", result); - response.setContentType("application/json"); return OAuthUtils.writeText(response, result, HttpStatus.SC_OK); } /** - * Verify the request by reviewing the values of client id, etc... + * Verify that all required OAuth 2.0 parameters are provided. * * @param clientId the client id * @param clientSecret the client secret * @throws InvalidParameterException with the name of the invalid parameter */ private void verifyRequest(final String clientId, final String clientSecret) throws InvalidParameterException { - // clientId is required + if (StringUtils.isBlank(clientId)) { - LOGGER.error("Missing {}", OAuthConstants.CLIENT_ID); + LOGGER.error(OAuthConstants.MISSING_CLIENT_ID_DESCRIPTION); throw new InvalidParameterException(OAuthConstants.CLIENT_ID); } - // clientSecret is required + if (StringUtils.isBlank(clientSecret)) { - LOGGER.error("Missing {}", OAuthConstants.CLIENT_SECRET); + LOGGER.error(OAuthConstants.MISSING_CLIENT_SECRET_DESCRIPTION); throw new InvalidParameterException(OAuthConstants.CLIENT_SECRET); } } diff --git a/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/web/OAuth20MetadataPrincipalController.java b/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/web/OAuth20MetadataPrincipalController.java index 7617d21e..7a2f4ef4 100644 --- a/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/web/OAuth20MetadataPrincipalController.java +++ b/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/web/OAuth20MetadataPrincipalController.java @@ -19,107 +19,154 @@ package org.jasig.cas.support.oauth.web; import com.fasterxml.jackson.databind.ObjectMapper; + import org.apache.commons.lang3.StringUtils; import org.apache.http.HttpStatus; + import org.jasig.cas.support.oauth.CentralOAuthService; +import org.jasig.cas.support.oauth.InvalidParameterException; import org.jasig.cas.support.oauth.OAuthConstants; import org.jasig.cas.support.oauth.OAuthUtils; import org.jasig.cas.support.oauth.metadata.PrincipalMetadata; import org.jasig.cas.support.oauth.token.AccessToken; import org.jasig.cas.support.oauth.token.InvalidTokenException; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; + import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.mvc.AbstractController; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + /** - * This controller handles requests for metadata regarding a principal. + * The OAuth 2.0 "Metadata Principal" Controller. + * + * This controller handles requests that ask for the metadata regarding a principal. The principal is identified and + * authenticated by the given access token. In addition, the access token must be of type CAS. * * @author Michael Haselton - * @since 4.1.0 + * @author Longze Chen + * @since 4.1.5 */ public final class OAuth20MetadataPrincipalController extends AbstractController { + /** Log instance for logging events, info, warnings, errors, etc. */ private static final Logger LOGGER = LoggerFactory.getLogger(OAuth20MetadataPrincipalController.class); - private static final String CLIENT_ID = "client_id"; - - private static final String NAME = "name"; - - private static final String DESCRIPTION = "description"; - - private static final String SCOPE = "scope"; - + /** The CAS OAuth authorization service. */ private final CentralOAuthService centralOAuthService; /** - * Instantiates a new o auth20 principal metadata controller. + * Instantiates a new {@link OAuth20MetadataPrincipalController}. * - * @param centralOAuthService the central oauth service + * @param centralOAuthService the CAS OAuth service */ public OAuth20MetadataPrincipalController(final CentralOAuthService centralOAuthService) { this.centralOAuthService = centralOAuthService; } @Override - protected ModelAndView handleRequestInternal(final HttpServletRequest request, final HttpServletResponse response) - throws Exception { + protected ModelAndView handleRequestInternal( + final HttpServletRequest request, + final HttpServletResponse response + ) throws Exception { + + // Verify that all required OAuth 2.0 parameters are provided. + final String prefixedBearerToken = request.getHeader(OAuthConstants.AUTHORIZATION_HEADER); + LOGGER.debug("{} : {}", OAuthConstants.BEARER_TOKEN, prefixedBearerToken); String accessTokenId = request.getParameter(OAuthConstants.ACCESS_TOKEN); - if (StringUtils.isBlank(accessTokenId)) { - final String authHeader = request.getHeader("Authorization"); - if (StringUtils.isNotBlank(authHeader) && authHeader.startsWith(OAuthConstants.BEARER_TOKEN + " ")) { - accessTokenId = authHeader.substring(OAuthConstants.BEARER_TOKEN.length() + 1); - } else { - LOGGER.debug("Missing Access Token"); - return OAuthUtils.writeJsonError(response, OAuthConstants.MISSING_ACCESS_TOKEN, - OAuthConstants.MISSING_ACCESS_TOKEN_DESCRIPTION, - HttpStatus.SC_BAD_REQUEST); - } + LOGGER.debug("{} : {}", OAuthConstants.ACCESS_TOKEN, accessTokenId); + try { + accessTokenId = verifyRequest(accessTokenId, prefixedBearerToken); + } catch (final InvalidParameterException e) { + return OAuthUtils.writeJsonError( + response, + OAuthConstants.INVALID_REQUEST, + e.getMessage(), + HttpStatus.SC_BAD_REQUEST + ); } + // Verify and retrieve the access token. final AccessToken accessToken; try { accessToken = centralOAuthService.getToken(accessTokenId, AccessToken.class); } catch (final InvalidTokenException e) { LOGGER.error("Could not get Access Token [{}]", accessTokenId); - return OAuthUtils.writeJsonError(response, OAuthConstants.UNAUTHORIZED_REQUEST, OAuthConstants.INVALID_ACCESS_TOKEN_DESCRIPTION, - HttpStatus.SC_UNAUTHORIZED); + return OAuthUtils.writeJsonError( + response, + OAuthConstants.UNAUTHORIZED_REQUEST, + OAuthConstants.INVALID_ACCESS_TOKEN_DESCRIPTION, + HttpStatus.SC_UNAUTHORIZED + ); } + // Retrieve the metadata about the principal identified and authenticated by the given access token. final Collection metadata; try { metadata = centralOAuthService.getPrincipalMetadata(accessToken); } catch (final InvalidTokenException e) { LOGGER.error("Invalid Access Token [{}] type [{}]", accessToken.getId(), accessToken.getType()); - return OAuthUtils.writeJsonError(response, OAuthConstants.UNAUTHORIZED_REQUEST, OAuthConstants.INVALID_ACCESS_TOKEN_DESCRIPTION, - HttpStatus.SC_UNAUTHORIZED); + return OAuthUtils.writeJsonError( + response, + OAuthConstants.UNAUTHORIZED_REQUEST, + OAuthConstants.INVALID_ACCESS_TOKEN_TYPE_DESCRIPTION, + HttpStatus.SC_UNAUTHORIZED + ); } + // Build and return the response. final List> metadataList = new ArrayList<>(); for (final PrincipalMetadata item : metadata) { final Map detailMap = new HashMap<>(); - detailMap.put(CLIENT_ID, item.getClientId()); - detailMap.put(NAME, item.getName()); - detailMap.put(DESCRIPTION, item.getDescription()); - detailMap.put(SCOPE, item.getScopes()); + detailMap.put(OAuthConstants.CLIENT_ID, item.getClientId()); + detailMap.put(OAuthConstants.SERVICE_NAME, item.getName()); + detailMap.put(OAuthConstants.SERVICE_DESCRIPTION, item.getDescription()); + detailMap.put(OAuthConstants.SCOPE, item.getScopes()); metadataList.add(detailMap); } - final Map map = new HashMap<>(); map.put("data", metadataList); - final String result = new ObjectMapper().writeValueAsString(map); LOGGER.debug("result : {}", result); - response.setContentType("application/json"); return OAuthUtils.writeText(response, result, HttpStatus.SC_OK); } + + /** + * Verify that all required OAuth 2.0 parameters are provided. + * + * @param accessTokenId the access token id + * @param prefixedBearerToken the prefixed bearer token provided by the HTTP Authorization header + * @return the access token id + * @throws InvalidParameterException with the name of the invalid parameter + */ + private String verifyRequest( + final String accessTokenId, + final String prefixedBearerToken + ) throws InvalidParameterException { + + // An access token must be provided via either the request body or the HTTP "Authorization" header. + if (StringUtils.isBlank(accessTokenId)) { + if (StringUtils.isNotBlank(prefixedBearerToken) + && prefixedBearerToken.startsWith(OAuthConstants.BEARER_TOKEN + " ") + ) { + return prefixedBearerToken.substring(OAuthConstants.BEARER_TOKEN.length() + 1); + } else { + LOGGER.debug(OAuthConstants.MISSING_ACCESS_TOKEN_DESCRIPTION); + throw new InvalidParameterException(OAuthConstants.ACCESS_TOKEN); + } + } else { + return accessTokenId; + } + } } diff --git a/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/web/OAuth20ProfileController.java b/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/web/OAuth20ProfileController.java index 8a0286b3..427f0770 100644 --- a/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/web/OAuth20ProfileController.java +++ b/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/web/OAuth20ProfileController.java @@ -19,11 +19,14 @@ package org.jasig.cas.support.oauth.web; import com.fasterxml.jackson.databind.ObjectMapper; + import org.apache.commons.lang3.StringUtils; import org.apache.http.HttpStatus; + import org.jasig.cas.CentralAuthenticationService; import org.jasig.cas.authentication.principal.Principal; import org.jasig.cas.support.oauth.CentralOAuthService; +import org.jasig.cas.support.oauth.InvalidParameterException; import org.jasig.cas.support.oauth.OAuthConstants; import org.jasig.cas.support.oauth.OAuthUtils; import org.jasig.cas.support.oauth.personal.PersonalAccessToken; @@ -33,135 +36,187 @@ import org.jasig.cas.ticket.InvalidTicketException; import org.jasig.cas.ticket.ServiceTicket; import org.jasig.cas.validation.Assertion; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; + import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.mvc.AbstractController; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; import java.util.HashMap; import java.util.Map; import java.util.Set; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + /** - * This controller returns a profile for the authenticated user - * (identifier + attributes), found with the access token (CAS granting - * ticket). + * The OAuth 2.0 "Profile" controller. + * + * This controller handles requests that ask for the profile (identifier, attributes and scopes) of the principal + * associated with a given access token. * * @author Jerome Leleu * @author Michael Haselton - * @since 3.5.0 + * @author Longze Chen + * @since 4.1.5 */ public final class OAuth20ProfileController extends AbstractController { + /** Log instance for logging events, info, warnings, errors, etc. */ private static final Logger LOGGER = LoggerFactory.getLogger(OAuth20ProfileController.class); - private static final String ID = "id"; - - private static final String ATTRIBUTES = "attributes"; - - private static final String SCOPE = "scope"; - + /** The CAS OAuth authorization service. */ private final CentralOAuthService centralOAuthService; + /** The primary CAS authentication service. */ private final CentralAuthenticationService centralAuthenticationService; /** - * Instantiates a new o auth20 profile controller. + * Instantiates a new {@link OAuth20ProfileController}. * - * @param centralOAuthService the central oauth service - * @param centralAuthenticationService the central authentication service + * @param centralOAuthService the CAS OAuth service + * @param centralAuthenticationService the primary CAS authentication service */ - public OAuth20ProfileController(final CentralOAuthService centralOAuthService, - final CentralAuthenticationService centralAuthenticationService) { + public OAuth20ProfileController( + final CentralOAuthService centralOAuthService, + final CentralAuthenticationService centralAuthenticationService + ) { this.centralOAuthService = centralOAuthService; this.centralAuthenticationService = centralAuthenticationService; } @Override - protected ModelAndView handleRequestInternal(final HttpServletRequest request, final HttpServletResponse response) - throws Exception { + protected ModelAndView handleRequestInternal( + final HttpServletRequest request, + final HttpServletResponse response + ) throws Exception { + + // Verify that all required OAuth 2.0 parameters are provided. + final String prefixedBearerToken = request.getHeader(OAuthConstants.AUTHORIZATION_HEADER); + LOGGER.debug("{} : {}", OAuthConstants.BEARER_TOKEN, prefixedBearerToken); String accessTokenId = request.getParameter(OAuthConstants.ACCESS_TOKEN); - if (StringUtils.isBlank(accessTokenId)) { - final String authHeader = request.getHeader("Authorization"); - if (StringUtils.isNotBlank(authHeader) && authHeader.startsWith(OAuthConstants.BEARER_TOKEN + " ")) { - accessTokenId = authHeader.substring(OAuthConstants.BEARER_TOKEN.length() + 1); - } else { - LOGGER.debug("Missing Access Token"); - return OAuthUtils.writeJsonError(response, OAuthConstants.MISSING_ACCESS_TOKEN, - OAuthConstants.MISSING_ACCESS_TOKEN_DESCRIPTION, - HttpStatus.SC_BAD_REQUEST); - } + LOGGER.debug("{} : {}", OAuthConstants.ACCESS_TOKEN, accessTokenId); + try { + accessTokenId = verifyRequest(accessTokenId, prefixedBearerToken); + } catch (final InvalidParameterException e) { + return OAuthUtils.writeJsonError( + response, + OAuthConstants.INVALID_REQUEST, + e.getMessage(), + HttpStatus.SC_BAD_REQUEST + ); } + // Verify and retrieve the access token. The token can be OAuth-granted by authorization or CAS-granted by + // authentication. It can also be an OSF-generated PERSONAL access token, which does not exist in the token + // registry (i.e. CAS database) the first time it is used. AccessToken accessToken; try { accessToken = centralOAuthService.getToken(accessTokenId, AccessToken.class); } catch (final InvalidTokenException e) { - // attempt to grant a personal access token? + // If not found, check if it is an PERSONAL access token. final PersonalAccessToken personalAccessToken = centralOAuthService.getPersonalAccessToken(accessTokenId); if (personalAccessToken != null) { accessToken = centralOAuthService.grantPersonalAccessToken(personalAccessToken); } else { LOGGER.error("Could not get Access Token [{}]", accessTokenId); - return OAuthUtils.writeJsonError(response, OAuthConstants.UNAUTHORIZED_REQUEST, + return OAuthUtils.writeJsonError( + response, + OAuthConstants.UNAUTHORIZED_REQUEST, OAuthConstants.INVALID_ACCESS_TOKEN_DESCRIPTION, - HttpStatus.SC_UNAUTHORIZED); + HttpStatus.SC_UNAUTHORIZED + ); } } - final ObjectMapper mapper = new ObjectMapper(); - final Map map = new HashMap<>(); - + // Retrieve the principal id and released attributes associated with the access token. final Principal principal; + final Map attributeMap = new HashMap<>(); if (accessToken.getType() == TokenType.PERSONAL) { - // personal access tokens do not have a service id, thus no attributes can be released, - // also need to grant service ticket here if we would like keep stats on ticket usage. + // PERSONAL access tokens do not have an OAuth registered service. Thus no attributes can be released. + // TODO: Need to grant a service ticket here if we would like keep stats on ticket usage. principal = accessToken.getTicketGrantingTicket().getAuthentication().getPrincipal(); } else { + // Retrieve an existing or grant a new service ticket. final ServiceTicket serviceTicket; if (accessToken.getType() == TokenType.OFFLINE) { + // OFFLINE access tokens are granted with a service ticket. Thus just use it here. serviceTicket = accessToken.getServiceTicket(); } else { - serviceTicket = centralAuthenticationService.grantServiceTicket(accessToken.getTicketGrantingTicket().getId(), - accessToken.getService()); + // Both ONLINE and CAS access tokens are granted with a ticket granting ticket and an associated + // registered service. Thus, must grant a new service ticket here using the TGT and the service. + serviceTicket = centralAuthenticationService + .grantServiceTicket(accessToken.getTicketGrantingTicket().getId(), accessToken.getService() + ); } - - // validate the service ticket, and apply service specific attribute release policy + // Validate the service ticket, and apply service specific attribute release policy final Assertion assertion; try { - assertion = centralAuthenticationService.validateServiceTicket(serviceTicket.getId(), serviceTicket.getService()); + assertion = centralAuthenticationService + .validateServiceTicket(serviceTicket.getId(), serviceTicket.getService()); } catch (final InvalidTicketException e) { - LOGGER.error("Could not validate Service Ticket [{}] of Access Token [{}] ", serviceTicket.getId(), accessToken.getId()); - return OAuthUtils.writeJsonError(response, OAuthConstants.UNAUTHORIZED_REQUEST, + LOGGER.error( + "Could not validate Service Ticket [{}] of Access Token [{}] ", + serviceTicket.getId(), + accessToken.getId() + ); + return OAuthUtils.writeJsonError( + response, + OAuthConstants.UNAUTHORIZED_REQUEST, OAuthConstants.INVALID_ACCESS_TOKEN_DESCRIPTION, - HttpStatus.SC_UNAUTHORIZED); + HttpStatus.SC_UNAUTHORIZED + ); } - + // Retrieve principal and attributes from the service ticket validation assertion. principal = assertion.getPrimaryAuthentication().getPrincipal(); - - final Map attributeMap = new HashMap<>(); for (final Map.Entry attribute : principal.getAttributes().entrySet()) { attributeMap.put(attribute.getKey(), attribute.getValue()); } - - if (attributeMap.size() > 0) { - map.put(ATTRIBUTES, attributeMap); - } } - map.put(ID, principal.getId()); - + // Build and return the response. + final ObjectMapper mapper = new ObjectMapper(); + final Map map = new HashMap<>(); + map.put(OAuthConstants.PRINCIPAL_ID, principal.getId()); + if (attributeMap.size() > 0) { + map.put(OAuthConstants.PRINCIPAL_ATTRIBUTES, attributeMap); + } final Set scopes = accessToken.getScopes(); if (scopes.size() > 0) { - map.put(SCOPE, accessToken.getScopes()); + map.put(OAuthConstants.SCOPE, accessToken.getScopes()); } - final String result = mapper.writeValueAsString(map); LOGGER.debug("result : {}", result); - response.setContentType("application/json"); return OAuthUtils.writeText(response, result, HttpStatus.SC_OK); } + + /** + * Verify that all required OAuth 2.0 parameters are provided. + * + * @param accessTokenId the access token id + * @param prefixedBearerToken the prefixed bearer token provided by the HTTP Authorization header + * @return the access token id + * @throws InvalidParameterException with the name of the invalid parameter + */ + private String verifyRequest( + final String accessTokenId, + final String prefixedBearerToken + ) throws InvalidParameterException { + + // An access token must be provided via either the request body or the HTTP "Authorization" header. + if (StringUtils.isBlank(accessTokenId)) { + if (StringUtils.isNotBlank(prefixedBearerToken) + && prefixedBearerToken.startsWith(OAuthConstants.BEARER_TOKEN + " ") + ) { + return prefixedBearerToken.substring(OAuthConstants.BEARER_TOKEN.length() + 1); + } else { + LOGGER.debug(OAuthConstants.MISSING_ACCESS_TOKEN_DESCRIPTION); + throw new InvalidParameterException(OAuthConstants.ACCESS_TOKEN); + } + } else { + return accessTokenId; + } + } } diff --git a/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/web/OAuth20RevokeClientPrincipalTokensController.java b/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/web/OAuth20RevokeClientPrincipalTokensController.java index 0822199a..c51bac3a 100644 --- a/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/web/OAuth20RevokeClientPrincipalTokensController.java +++ b/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/web/OAuth20RevokeClientPrincipalTokensController.java @@ -20,13 +20,17 @@ import org.apache.commons.lang3.StringUtils; import org.apache.http.HttpStatus; + import org.jasig.cas.support.oauth.CentralOAuthService; +import org.jasig.cas.support.oauth.InvalidParameterException; import org.jasig.cas.support.oauth.OAuthConstants; import org.jasig.cas.support.oauth.OAuthUtils; import org.jasig.cas.support.oauth.token.AccessToken; import org.jasig.cas.support.oauth.token.InvalidTokenException; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; + import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.mvc.AbstractController; @@ -34,59 +38,120 @@ import javax.servlet.http.HttpServletResponse; /** - * This controller handles requests revoke all tokens associated with a client id and principal. + * The OAuth 2.0 revoke client-principal tokens controller. + * + * This controller handles requests that revoke all of a given user's tokens for a registered service. The service is + * identified by the client id and the user by the principal id. + * + * As mentioned in {@link CentralOAuthService#revokeClientPrincipalTokens}, currently this request does not support + * PERSONAL access tokens. For CAS access tokens, any client id can be used. However, for OFFLINE and ONLINE access + * tokens, only the client id with which the token is associated works. * * @author Michael Haselton - * @since 4.1.0 + * @author Longze Chen + * @since 4.1.5 */ public final class OAuth20RevokeClientPrincipalTokensController extends AbstractController { + /** Log instance for logging events, info, warnings, errors, etc. */ private static final Logger LOGGER = LoggerFactory.getLogger(OAuth20RevokeClientPrincipalTokensController.class); + /** The CAS oauth authorization service. */ private final CentralOAuthService centralOAuthService; /** - * Instantiates a new o auth20 revoke client principal tokens controller. + * Instantiates a new {@link OAuth20RevokeClientPrincipalTokensController}. * - * @param centralOAuthService the central oauth service + * @param centralOAuthService the CAS OAuth service */ public OAuth20RevokeClientPrincipalTokensController(final CentralOAuthService centralOAuthService) { this.centralOAuthService = centralOAuthService; } @Override - protected ModelAndView handleRequestInternal(final HttpServletRequest request, final HttpServletResponse response) - throws Exception { + protected ModelAndView handleRequestInternal( + final HttpServletRequest request, + final HttpServletResponse response + ) throws Exception { + + // Verify that all required OAuth 2.0 parameters are provided. final String clientId = request.getParameter(OAuthConstants.CLIENT_ID); LOGGER.debug("{} : {}", OAuthConstants.CLIENT_ID, clientId); - + final String prefixedBearerToken = request.getHeader(OAuthConstants.AUTHORIZATION_HEADER); + LOGGER.debug("{} : {}", OAuthConstants.BEARER_TOKEN, prefixedBearerToken); String accessTokenId = request.getParameter(OAuthConstants.ACCESS_TOKEN); - if (StringUtils.isBlank(accessTokenId)) { - final String authHeader = request.getHeader("Authorization"); - if (StringUtils.isNotBlank(authHeader) && authHeader.startsWith(OAuthConstants.BEARER_TOKEN + " ")) { - accessTokenId = authHeader.substring(OAuthConstants.BEARER_TOKEN.length() + 1); - } else { - LOGGER.debug("Missing Access Token"); - return OAuthUtils.writeJsonError(response, OAuthConstants.INVALID_REQUEST, OAuthConstants.MISSING_ACCESS_TOKEN_DESCRIPTION, - HttpStatus.SC_BAD_REQUEST); - } + LOGGER.debug("{} : {}", OAuthConstants.ACCESS_TOKEN, accessTokenId); + try { + accessTokenId = verifyRequest(clientId, accessTokenId, prefixedBearerToken); + } catch (final InvalidParameterException e) { + return OAuthUtils.writeJsonError( + response, + OAuthConstants.INVALID_REQUEST, + e.getMessage(), + HttpStatus.SC_BAD_REQUEST + ); } + // Retrieve the access token from the token registry via the CAS OAuth service. final AccessToken accessToken; try { accessToken = centralOAuthService.getToken(accessTokenId, AccessToken.class); } catch (final InvalidTokenException e) { LOGGER.error("Could not get Access Token [{}]", accessTokenId); - return OAuthUtils.writeJsonError(response, OAuthConstants.UNAUTHORIZED_REQUEST, OAuthConstants.INVALID_ACCESS_TOKEN_DESCRIPTION, - HttpStatus.SC_UNAUTHORIZED); + return OAuthUtils.writeJsonError( + response, + OAuthConstants.UNAUTHORIZED_REQUEST, + OAuthConstants.INVALID_ACCESS_TOKEN_DESCRIPTION, + HttpStatus.SC_UNAUTHORIZED + ); } + // Attempt to revoke all of the principal's tokens for the registered service associated with the client id. + // Return an HTTP 204 No Content response upon success or HTTP 400 Bad Request upon failure. if (!centralOAuthService.revokeClientPrincipalTokens(accessToken, clientId)) { LOGGER.error("Could not revoke client principal tokens"); - return OAuthUtils.writeJsonError(response, OAuthConstants.INVALID_REQUEST, OAuthConstants.INVALID_ACCESS_TOKEN_DESCRIPTION, - HttpStatus.SC_BAD_REQUEST); + return OAuthUtils.writeJsonError( + response, + OAuthConstants.INVALID_REQUEST, + OAuthConstants.INVALID_ACCESS_TOKEN_DESCRIPTION, + HttpStatus.SC_BAD_REQUEST + ); } - return OAuthUtils.writeText(response, null, HttpStatus.SC_NO_CONTENT); } + + /** + * Verify that all required OAuth 2.0 parameters are provided. + * + * @param clientId the client id + * @param accessTokenId the access token id + * @param prefixedBearerToken the prefixed bearer token provided by the HTTP Authorization header + * @return the access token id + * @throws InvalidParameterException with the name of the invalid parameter + */ + private String verifyRequest( + final String clientId, + final String accessTokenId, + final String prefixedBearerToken + ) throws InvalidParameterException { + + if (StringUtils.isBlank(clientId)) { + LOGGER.error(OAuthConstants.MISSING_CLIENT_ID_DESCRIPTION); + throw new InvalidParameterException(OAuthConstants.CLIENT_ID); + } + + // An access token must be provided via either the request body or the "Authorization" header. + if (StringUtils.isBlank(accessTokenId)) { + if (StringUtils.isNotBlank(prefixedBearerToken) + && prefixedBearerToken.startsWith(OAuthConstants.BEARER_TOKEN + " ") + ) { + return prefixedBearerToken.substring(OAuthConstants.BEARER_TOKEN.length() + 1); + } else { + LOGGER.debug(OAuthConstants.MISSING_ACCESS_TOKEN_DESCRIPTION); + throw new InvalidParameterException(OAuthConstants.ACCESS_TOKEN); + } + } else { + return accessTokenId; + } + } } diff --git a/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/web/OAuth20RevokeClientTokensController.java b/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/web/OAuth20RevokeClientTokensController.java index 8c710d29..374e5337 100644 --- a/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/web/OAuth20RevokeClientTokensController.java +++ b/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/web/OAuth20RevokeClientTokensController.java @@ -20,6 +20,7 @@ import org.apache.commons.lang3.StringUtils; import org.apache.http.HttpStatus; + import org.jasig.cas.support.oauth.CentralOAuthService; import org.jasig.cas.support.oauth.InvalidParameterException; import org.jasig.cas.support.oauth.OAuthConstants; @@ -27,17 +28,22 @@ import org.jasig.cas.support.oauth.services.OAuthRegisteredService; import org.jasig.cas.support.oauth.token.AccessToken; import org.jasig.cas.support.oauth.token.RefreshToken; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; + import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.mvc.AbstractController; +import java.util.Collection; + import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -import java.util.Collection; /** - * This controller handles requests to revoke all tokens associated with a client id. + * The OAuth 2.0 "Revoke Client Tokens" controller. + * + * This controller handles requests to revoke all tokens of the registered service associated with a given client id. * * @author Michael Haselton * @author Longze Chen @@ -45,35 +51,44 @@ */ public final class OAuth20RevokeClientTokensController extends AbstractController { + /** Log instance for logging events, info, warnings, errors, etc. */ private static final Logger LOGGER = LoggerFactory.getLogger(OAuth20RevokeClientTokensController.class); + /** The CAS OAuth authorization service. */ private final CentralOAuthService centralOAuthService; /** - * Instantiates a new o auth20 revoke client tokens controller. + * Instantiates a new {@link OAuth20RevokeClientTokensController}. * - * @param centralOAuthService the central oauth service + * @param centralOAuthService the CAS OAuth service */ public OAuth20RevokeClientTokensController(final CentralOAuthService centralOAuthService) { this.centralOAuthService = centralOAuthService; } @Override - protected ModelAndView handleRequestInternal(final HttpServletRequest request, final HttpServletResponse response) - throws Exception { + protected ModelAndView handleRequestInternal( + final HttpServletRequest request, + final HttpServletResponse response + ) throws Exception { + + // Verify that all required OAuth 2.0 parameters are provided. final String clientId = request.getParameter(OAuthConstants.CLIENT_ID); LOGGER.debug("{} : {}", OAuthConstants.CLIENT_ID, clientId); - final String clientSecret = request.getParameter(OAuthConstants.CLIENT_SECRET); LOGGER.debug("{} : {}", OAuthConstants.CLIENT_SECRET, "************"); - try { verifyRequest(clientId, clientSecret); } catch (final InvalidParameterException e) { - return OAuthUtils.writeJsonError(response, OAuthConstants.INVALID_REQUEST, e.getMessage(), HttpStatus.SC_BAD_REQUEST); + return OAuthUtils.writeJsonError( + response, + OAuthConstants.INVALID_REQUEST, + e.getMessage(), + HttpStatus.SC_BAD_REQUEST + ); } - // Verify that the client service has a valid client id and client secret + // Retrieve the registered service associated with the client id and verify the client secret. final OAuthRegisteredService service = centralOAuthService.getRegisteredService(clientId); if (service == null || !service.getClientSecret().equals(clientSecret)) { LOGGER.error("Could not revoke client tokens, mismatched client id or client secret"); @@ -85,38 +100,38 @@ protected ModelAndView handleRequestInternal(final HttpServletRequest request, f ); } - // Remove all refresh tokens for the client of the specified id + // Attempt to remove all refresh and access tokens associated with the registered service via client id. final Collection refreshTokens = centralOAuthService.getClientRefreshTokens(clientId); for (final RefreshToken token: refreshTokens) { LOGGER.debug("Revoking refresh token : {}", token.getId()); centralOAuthService.revokeToken(token); } - // Remove all access tokens for the client of the specified id final Collection accessTokens = centralOAuthService.getClientAccessTokens(clientId); for (final AccessToken token: accessTokens) { LOGGER.info("Revoking access token : {}", token.getId()); centralOAuthService.revokeToken(token); } + // Return an HTTP 204 No Content after all tokens have been removed successfully. return OAuthUtils.writeText(response, null, HttpStatus.SC_NO_CONTENT); } /** - * Verify the request by reviewing the values of client id, etc... + * Verify that all required OAuth 2.0 parameters are provided. * * @param clientId the client id * @param clientSecret the client secret * @throws InvalidParameterException with the name of the invalid parameter */ private void verifyRequest(final String clientId, final String clientSecret) throws InvalidParameterException { - // clientId is required + if (StringUtils.isBlank(clientId)) { - LOGGER.error("Missing {}", OAuthConstants.CLIENT_ID); + LOGGER.error(OAuthConstants.MISSING_CLIENT_ID_DESCRIPTION); throw new InvalidParameterException(OAuthConstants.CLIENT_ID); } - // clientSecret is required + if (StringUtils.isBlank(clientSecret)) { - LOGGER.error("Missing {}", OAuthConstants.CLIENT_SECRET); + LOGGER.error(OAuthConstants.MISSING_CLIENT_SECRET_DESCRIPTION); throw new InvalidParameterException(OAuthConstants.CLIENT_SECRET); } } diff --git a/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/web/OAuth20RevokeTokenController.java b/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/web/OAuth20RevokeTokenController.java index bd3fd650..976e6316 100644 --- a/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/web/OAuth20RevokeTokenController.java +++ b/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/web/OAuth20RevokeTokenController.java @@ -18,14 +18,19 @@ */ package org.jasig.cas.support.oauth.web; +import org.apache.commons.lang3.StringUtils; import org.apache.http.HttpStatus; + import org.jasig.cas.support.oauth.CentralOAuthService; +import org.jasig.cas.support.oauth.InvalidParameterException; import org.jasig.cas.support.oauth.OAuthConstants; import org.jasig.cas.support.oauth.OAuthUtils; import org.jasig.cas.support.oauth.token.InvalidTokenException; import org.jasig.cas.support.oauth.token.Token; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; + import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.mvc.AbstractController; @@ -33,46 +38,89 @@ import javax.servlet.http.HttpServletResponse; /** - * This controller handles requests to revoke access tokens and refresh tokens. + * The OAuth 2.0 "Revoke Token" controller. + * + * This controller handles requests to revoke one access or refresh token. * * @author Michael Haselton - * @since 4.1.0 + * @author Longze Chen + * @since 4.1.5 */ public final class OAuth20RevokeTokenController extends AbstractController { + /** Log instance for logging events, info, warnings, errors, etc. */ private static final Logger LOGGER = LoggerFactory.getLogger(OAuth20RevokeTokenController.class); + /** The CAS OAuth authorization service. */ private final CentralOAuthService centralOAuthService; /** - * Instantiates a new oauth2 revoke user token controller. + * Instantiates a {@link OAuth20RevokeTokenController}. * - * @param centralOAuthService the central oauth service + * @param centralOAuthService the CAS OAuth service */ public OAuth20RevokeTokenController(final CentralOAuthService centralOAuthService) { this.centralOAuthService = centralOAuthService; } @Override - protected ModelAndView handleRequestInternal(final HttpServletRequest request, final HttpServletResponse response) - throws Exception { + protected ModelAndView handleRequestInternal( + final HttpServletRequest request, + final HttpServletResponse response + ) throws Exception { + + // Verify that all required OAuth 2.0 parameters are provided. final String tokenId = request.getParameter(OAuthConstants.TOKEN); LOGGER.debug("{} : {}", OAuthConstants.TOKEN, tokenId); + try { + verifyRequest(tokenId); + } catch (final InvalidParameterException e) { + return OAuthUtils.writeJsonError( + response, + OAuthConstants.INVALID_REQUEST, + e.getMessage(), + HttpStatus.SC_BAD_REQUEST + ); + } + // Verify that the token is valid. final Token token; try { token = centralOAuthService.getToken(tokenId); } catch (final InvalidTokenException e) { - LOGGER.error("Invalid token : {}", tokenId); - return OAuthUtils.writeJsonError(response, OAuthConstants.INVALID_REQUEST, e.getMessage(), HttpStatus.SC_BAD_REQUEST); + LOGGER.error("{} : {}", OAuthConstants.INVALID_TOKEN_DESCRIPTION, tokenId); + return OAuthUtils.writeJsonError( + response, + OAuthConstants.INVALID_REQUEST, + e.getMessage(), + HttpStatus.SC_BAD_REQUEST + ); } + // Attempt to revoke the token, return an HTTP 204 No Content if successful or an HTTP 400 otherwise. if (!centralOAuthService.revokeToken(token)) { LOGGER.error("Token revocation failed [{}]", token.getId()); - return OAuthUtils.writeJsonError(response, OAuthConstants.INVALID_REQUEST, OAuthConstants.FAILED_TOKEN_REVOCATION_DESCRIPTION, - HttpStatus.SC_BAD_REQUEST); + return OAuthUtils.writeJsonError( + response, + OAuthConstants.INVALID_REQUEST, + OAuthConstants.FAILED_TOKEN_REVOCATION_DESCRIPTION, + HttpStatus.SC_BAD_REQUEST + ); } - return OAuthUtils.writeText(response, null, HttpStatus.SC_NO_CONTENT); } + + /** + * Verify that all required OAuth 2.0 parameters are provided. + * + * @param tokenId the token id + * @throws InvalidParameterException with the name of the invalid parameter + */ + private void verifyRequest(final String tokenId) throws InvalidParameterException { + + if (StringUtils.isBlank(tokenId)) { + LOGGER.error(OAuthConstants.MISSING_TOKEN_DESCRIPTION); + throw new InvalidParameterException(OAuthConstants.TOKEN); + } + } } diff --git a/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/web/OAuth20ServiceValidateController.java b/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/web/OAuth20ServiceValidateController.java index 2a8309f2..d2dae7c9 100644 --- a/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/web/OAuth20ServiceValidateController.java +++ b/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/web/OAuth20ServiceValidateController.java @@ -16,22 +16,21 @@ * specific language governing permissions and limitations * under the License. */ - package org.jasig.cas.support.oauth.web; - -import org.jasig.cas.CentralAuthenticationService; import org.jasig.cas.authentication.principal.WebApplicationService; +import org.jasig.cas.CentralAuthenticationService; import org.jasig.cas.services.ServicesManager; import org.jasig.cas.support.oauth.CentralOAuthService; import org.jasig.cas.support.oauth.OAuthConstants; import org.jasig.cas.support.oauth.token.AccessToken; +import org.jasig.cas.ticket.proxy.ProxyHandler; import org.jasig.cas.ticket.ServiceTicket; import org.jasig.cas.ticket.Ticket; -import org.jasig.cas.ticket.proxy.ProxyHandler; import org.jasig.cas.web.DelegateController; import org.jasig.cas.web.ServiceValidateController; import org.jasig.cas.web.support.ArgumentExtractor; + import org.springframework.context.ApplicationContext; import org.springframework.web.servlet.ModelAndView; @@ -40,21 +39,26 @@ import javax.validation.constraints.NotNull; /** - * This controller allows injection of an oauth access token into the CAS protocol. + * The OAuth 2.0 Service Validation Controller. + * + * This controller allows injection of an OAuth access token into the CAS protocol. With current CAS settings, this + * is the base / parent controller class for the service validation process in both CAS 2.0 and 3.0 protocol, which + * replaces the default {@link ServiceValidateController} by wrapping it into the class as a private property. * * @author Michael Haselton - * @since 4.1.0 + * @author Longze Chen + * @since 4.1.5 */ public class OAuth20ServiceValidateController extends DelegateController { - /** Wrapped Service Validate Controller. */ + /** The wrapped service validate controller. */ private ServiceValidateController wrapped = new ServiceValidateController(); - /** The central oauth service. */ + /** The CAS OAuth authorization service. */ @NotNull private CentralOAuthService centralOAuthService; - /** The central authentication service. */ + /** The primary CAS authentication service. */ @NotNull private CentralAuthenticationService centralAuthenticationService; @@ -62,13 +66,20 @@ public class OAuth20ServiceValidateController extends DelegateController { @NotNull private String successView; - /** Extracts parameters from Request object. */ + /** + * Extracts parameters from a given {@link HttpServletRequest} object. + */ @NotNull private ArgumentExtractor argumentExtractor; /** - * Calls {@link #initServletContext(javax.servlet.ServletContext)} if the - * given ApplicationContext is a WebApplicationContext. + * Initialize the application context. + * + * It calls the super {@link #initApplicationContext} first to initialize the application context. Then it sets the + * context for the wrapped service validated controller {@link #wrapped}. + * + * Note: {@link #initApplicationContext} calls {@link #initServletContext} if the given {@link ApplicationContext} + * object is an instance of {@link org.springframework.web.context.WebApplicationContext}. */ @Override protected void initApplicationContext(final ApplicationContext context) { @@ -77,7 +88,11 @@ protected void initApplicationContext(final ApplicationContext context) { } @Override - public ModelAndView handleRequest(final HttpServletRequest request, final HttpServletResponse response) throws Exception { + public ModelAndView handleRequest( + final HttpServletRequest request, + final HttpServletResponse response + ) throws Exception { + final WebApplicationService service = this.argumentExtractor.extractService(request); final String serviceTicketId = service != null ? service.getArtifactId() : null; @@ -91,8 +106,8 @@ public ModelAndView handleRequest(final HttpServletRequest request, final HttpSe final ModelAndView modelAndView = wrapped.handleRequest(request, response); if (service != null && serviceTicket != null && modelAndView.getViewName().equals(this.successView)) { - final AccessToken accessToken = centralOAuthService.grantCASAccessToken(serviceTicket.getGrantingTicket(), - serviceTicket.getService()); + final AccessToken accessToken = centralOAuthService + .grantCASAccessToken(serviceTicket.getGrantingTicket(), serviceTicket.getService()); modelAndView.addObject(OAuthConstants.CAS_PROTOCOL_ACCESS_TOKEN, accessToken.getId()); modelAndView.addObject(OAuthConstants.CAS_PROTOCOL_ACCESS_TOKEN_SCOPE, accessToken.getScopes()); } @@ -101,13 +116,23 @@ public ModelAndView handleRequest(final HttpServletRequest request, final HttpSe } @Override - protected ModelAndView handleRequestInternal(final HttpServletRequest httpServletRequest, final HttpServletResponse httpServletResponse) - throws Exception { + protected ModelAndView handleRequestInternal( + final HttpServletRequest httpServletRequest, + final HttpServletResponse httpServletResponse + ) throws Exception { + // This class must be implemented to meet the interface requirements. It is expected to be used by the + // `handleRequest()` method from the `org.springframework.web.servlet.mvc.AbstractController`. However, + // we have overridden and fully rewritten the that method above. There is no need for this method to do + // anything at all and thus return `null`. return null; } /** - * {@inheritDoc} + * Determine if this class (i.e. a subclass of {@link DelegateController} can handle the current request. + * + * @param request the current request + * @param response the response + * @return true if the controller can handler the request, false otherwise */ @Override public boolean canHandle(final HttpServletRequest request, final HttpServletResponse response) { @@ -115,8 +140,9 @@ public boolean canHandle(final HttpServletRequest request, final HttpServletResp } /** - * @param centralAuthenticationService The centralAuthenticationService to - * set. + * Set the primary CAS authentication service for this class and the {@link #wrapped} service validate controller. + * + * @param centralAuthenticationService the CAS authentication service to set */ public void setCentralAuthenticationService(final CentralAuthenticationService centralAuthenticationService) { this.centralAuthenticationService = centralAuthenticationService; @@ -124,16 +150,18 @@ public void setCentralAuthenticationService(final CentralAuthenticationService c } /** - * @param centralOAuthService The centralOAuthService to - * set. + * Set the CAS OAuth service. + * + * @param centralOAuthService the CAS OAuth service to set */ public void setCentralOAuthService(final CentralOAuthService centralOAuthService) { this.centralOAuthService = centralOAuthService; } /** - * @param argumentExtractor The argumentExtractor to - * set. + * Set the argument extractor for this class and the {@link #wrapped} service validate controller. + * + * @param argumentExtractor argument extractor to set */ public void setArgumentExtractor(final ArgumentExtractor argumentExtractor) { this.argumentExtractor = argumentExtractor; @@ -141,22 +169,27 @@ public void setArgumentExtractor(final ArgumentExtractor argumentExtractor) { } /** - * @param validationSpecificationClass The authenticationSpecificationClass - * to set. + * Set the validation specification class for the {@link #wrapped} service validate controller. + * + * @param validationSpecificationClass the validation specification class to set */ public void setValidationSpecificationClass(final Class validationSpecificationClass) { wrapped.setValidationSpecificationClass(validationSpecificationClass); } /** - * @param failureView The failureView to set. + * Set the failure view for the {@link #wrapped} service validate controller. + * + * @param failureView the failure view to set */ public void setFailureView(final String failureView) { wrapped.setFailureView(failureView); } /** - * @param successView The successView to set. + * Set the success view for this class and the {@link #wrapped} service validate controller. + * + * @param successView the success view to set */ public void setSuccessView(final String successView) { this.successView = successView; @@ -164,16 +197,18 @@ public void setSuccessView(final String successView) { } /** - * @param proxyHandler The proxyHandler to set. + * Set the proxy handler for the {@link #wrapped} service validate controller. + * + * @param proxyHandler the proxy handler to set */ public void setProxyHandler(final ProxyHandler proxyHandler) { wrapped.setProxyHandler(proxyHandler); } /** - * Sets the services manager. + * Set the services manager for the {@link #wrapped} service validate controller. * - * @param servicesManager the new services manager + * @param servicesManager the services manager to set */ public void setServicesManager(final ServicesManager servicesManager) { wrapped.setServicesManager(servicesManager); diff --git a/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/web/OAuth20TokenAuthorizationCodeController.java b/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/web/OAuth20TokenAuthorizationCodeController.java index ac67dc68..f6ee6485 100644 --- a/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/web/OAuth20TokenAuthorizationCodeController.java +++ b/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/web/OAuth20TokenAuthorizationCodeController.java @@ -19,8 +19,10 @@ package org.jasig.cas.support.oauth.web; import com.fasterxml.jackson.databind.ObjectMapper; + import org.apache.commons.lang3.StringUtils; import org.apache.http.HttpStatus; + import org.jasig.cas.support.oauth.CentralOAuthService; import org.jasig.cas.support.oauth.InvalidParameterException; import org.jasig.cas.support.oauth.OAuthConstants; @@ -31,21 +33,33 @@ import org.jasig.cas.support.oauth.token.InvalidTokenException; import org.jasig.cas.support.oauth.token.RefreshToken; import org.jasig.cas.support.oauth.token.TokenType; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; + import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.mvc.AbstractController; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; +import java.util.concurrent.TimeUnit; import java.util.HashMap; import java.util.Map; -import java.util.concurrent.TimeUnit; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; /** - * This controller handles requests for grant type authorization code, - * returning an access token which is the CAS service ticket and a refresh token - * which is the CAS granting ticket according to the service and code (service ticket) given. + * The OAuth 2.0 token-code exchange controller. + * + * This controller handles requests that ask for exchanging an authorization code for an ONLINE access token or an + * OFFLINE refresh token after the initial authorization process where "code" is used for the response type. + * + * ONLINE access token is granted via {@link CentralOAuthService#grantOnlineAccessToken} with a new ticket granting + * ticket generated with the OAuth credentials. + * + * Similarly, the OFFLINE refresh token is granted via {@link CentralOAuthService#grantOfflineRefreshToken} with a new + * ticket granting ticket generated with the OAuth credentials. In addition, an ONLINE access token is granted via + * {@link CentralOAuthService#grantOfflineAccessToken} with a new service ticket generated from the ticket granting + * ticket of the OFFLINE refresh token. * * @author Jerome Leleu * @author Michael Haselton @@ -54,17 +68,20 @@ */ public final class OAuth20TokenAuthorizationCodeController extends AbstractController { + /** Log instance for logging events, info, warnings, errors, etc. */ private static final Logger LOGGER = LoggerFactory.getLogger(OAuth20TokenAuthorizationCodeController.class); + /** The CAS OAuth authorization service. */ private final CentralOAuthService centralOAuthService; + /** The ticket timeout. */ private final Long timeout; /** - * Instantiates a new o auth20 grant type authorization code controller. + * Instantiates a new {@link OAuth20TokenAuthorizationCodeController}. * - * @param centralOAuthService the central oauth service - * @param timeout the timeout + * @param centralOAuthService the CAS OAuth service + * @param timeout the ticket timeout */ public OAuth20TokenAuthorizationCodeController(final CentralOAuthService centralOAuthService, final Long timeout) { this.centralOAuthService = centralOAuthService; @@ -72,123 +89,168 @@ public OAuth20TokenAuthorizationCodeController(final CentralOAuthService central } @Override - protected ModelAndView handleRequestInternal(final HttpServletRequest request, final HttpServletResponse response) - throws Exception { + protected ModelAndView handleRequestInternal( + final HttpServletRequest request, + final HttpServletResponse response + ) throws Exception { + + // Verify that all required OAuth parameters are provided. final String code = request.getParameter(OAuthConstants.CODE); LOGGER.debug("{} : {}", OAuthConstants.CODE, code); - final String clientId = request.getParameter(OAuthConstants.CLIENT_ID); LOGGER.debug("{} : {}", OAuthConstants.CLIENT_ID, clientId); - final String clientSecret = request.getParameter(OAuthConstants.CLIENT_SECRET); LOGGER.debug("{} : {}", OAuthConstants.CLIENT_SECRET, "*********"); - final String redirectUri = request.getParameter(OAuthConstants.REDIRECT_URI); LOGGER.debug("{} : {}", OAuthConstants.REDIRECT_URI, redirectUri); - final String grantType = request.getParameter(OAuthConstants.GRANT_TYPE); LOGGER.debug("{} : {}", OAuthConstants.GRANT_TYPE, grantType); - try { verifyRequest(redirectUri, clientId, clientSecret, code, grantType); } catch (final InvalidParameterException e) { - return OAuthUtils.writeJsonError(response, OAuthConstants.INVALID_REQUEST, e.getMessage(), HttpStatus.SC_BAD_REQUEST); + return OAuthUtils.writeJsonError( + response, + OAuthConstants.INVALID_REQUEST, + e.getMessage(), + HttpStatus.SC_BAD_REQUEST + ); } + // Verify that the authorization code is valid. final AuthorizationCode authorizationCode; try { authorizationCode = centralOAuthService.getToken(code, AuthorizationCode.class); } catch (final InvalidTokenException e) { LOGGER.error("Unknown {} : {}", OAuthConstants.AUTHORIZATION_CODE, code); - return OAuthUtils.writeJsonError(response, OAuthConstants.INVALID_REQUEST, - OAuthConstants.INVALID_CODE_DESCRIPTION, HttpStatus.SC_BAD_REQUEST); + return OAuthUtils.writeJsonError( + response, + OAuthConstants.INVALID_REQUEST, + OAuthConstants.INVALID_CODE_DESCRIPTION, + HttpStatus.SC_BAD_REQUEST + ); } + // Retrieve the registered service by client id. final OAuthRegisteredService service = centralOAuthService.getRegisteredService(clientId); if (service == null) { - LOGGER.error("Unknown {} : {}", OAuthConstants.CLIENT_ID, clientId); - return OAuthUtils.writeJsonError(response, OAuthConstants.INVALID_REQUEST, - OAuthConstants.INVALID_CLIENT_ID_OR_SECRET_DESCRIPTION, HttpStatus.SC_BAD_REQUEST); + // Log the "unknown client id" error while return a general "invalid client id or secret" message. + LOGGER.error("{} : {}", OAuthConstants.UNKNOWN_CLIENT_ID_DESCRIPTION, clientId); + return OAuthUtils.writeJsonError( + response, + OAuthConstants.INVALID_REQUEST, + OAuthConstants.INVALID_CLIENT_ID_OR_SECRET_DESCRIPTION, + HttpStatus.SC_BAD_REQUEST + ); } + + // Verify the client secret. if (!service.getClientSecret().equals(clientSecret)) { - LOGGER.error("Mismatched Client Secret parameters"); - return OAuthUtils.writeJsonError(response, OAuthConstants.INVALID_REQUEST, - OAuthConstants.INVALID_CLIENT_ID_OR_SECRET_DESCRIPTION, HttpStatus.SC_BAD_REQUEST); + // Log the "invalid client secret" error while return a general "invalid client id or secret" message. + LOGGER.error(OAuthConstants.INVALID_CLIENT_SECRET_DESCRIPTION); + return OAuthUtils.writeJsonError( + response, + OAuthConstants.INVALID_REQUEST, + OAuthConstants.INVALID_CLIENT_ID_OR_SECRET_DESCRIPTION, + HttpStatus.SC_BAD_REQUEST + ); } - // Redirect URI is a literal string (not regex) and the match is done by case-insensitive equality check. + + // Verify the redirect uri, which is stored as the `serviceId` property of an `OAuthRegisteredService` object. + // It is a literal string instead of a regular expression. Thus the match is done by a case-insensitive string + // equality check. if (!redirectUri.equalsIgnoreCase(service.getServiceId())) { - LOGGER.error("Unsupported {} : {} for serviceId : {}", OAuthConstants.REDIRECT_URI, redirectUri, service.getServiceId()); - return OAuthUtils.writeJsonError(response, OAuthConstants.INVALID_REQUEST, - OAuthConstants.INVALID_REDIRECT_URI_DESCRIPTION, HttpStatus.SC_BAD_REQUEST); + LOGGER.error( + "{} : {} for serviceId : {}", + OAuthConstants.INVALID_REDIRECT_URI_DESCRIPTION, + redirectUri, + service.getServiceId() + ); + return OAuthUtils.writeJsonError( + response, + OAuthConstants.INVALID_REQUEST, + OAuthConstants.INVALID_REDIRECT_URI_DESCRIPTION, + HttpStatus.SC_BAD_REQUEST + ); } - final Map map = new HashMap<>(); - + // Grant the access and refresh token. final AccessToken accessToken; + RefreshToken refreshToken = null; if (authorizationCode.getType() == TokenType.OFFLINE) { - final RefreshToken refreshToken = centralOAuthService.grantOfflineRefreshToken(authorizationCode, redirectUri); - map.put(OAuthConstants.REFRESH_TOKEN, refreshToken.getId()); - + refreshToken = centralOAuthService + .grantOfflineRefreshToken(authorizationCode, redirectUri); accessToken = centralOAuthService.grantOfflineAccessToken(refreshToken); } else if (authorizationCode.getType() == TokenType.ONLINE) { accessToken = centralOAuthService.grantOnlineAccessToken(authorizationCode); } else { - return OAuthUtils.writeJsonError(response, OAuthConstants.INVALID_GRANT, - OAuthConstants.INVALID_GRANT_TYPE_DESCRIPTION, HttpStatus.SC_BAD_REQUEST); + return OAuthUtils.writeJsonError( + response, + OAuthConstants.INVALID_GRANT, + OAuthConstants.INVALID_GRANT_TYPE_DESCRIPTION, + HttpStatus.SC_BAD_REQUEST + ); } - map.put(OAuthConstants.ACCESS_TOKEN, accessToken.getId()); - map.put(OAuthConstants.EXPIRES_IN, - (int) (timeout - TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis() - accessToken.getTicket().getCreationTime()))); + // Build and return the response. + final Map map = new HashMap<>(); map.put(OAuthConstants.TOKEN_TYPE, OAuthConstants.BEARER_TOKEN); - + if (authorizationCode.getType() == TokenType.OFFLINE && refreshToken != null) { + map.put(OAuthConstants.REFRESH_TOKEN, refreshToken.getId()); + } + map.put(OAuthConstants.ACCESS_TOKEN, accessToken.getId()); + final long timeSinceTicketCreation = System.currentTimeMillis() - accessToken.getTicket().getCreationTime(); + final int expiresIn = (int) (timeout - TimeUnit.MILLISECONDS.toSeconds(timeSinceTicketCreation)); + map.put(OAuthConstants.EXPIRES_IN, expiresIn); final ObjectMapper mapper = new ObjectMapper(); final String result = mapper.writeValueAsString(map); LOGGER.debug("result : {}", result); - response.setContentType("application/json"); return OAuthUtils.writeText(response, result, HttpStatus.SC_OK); } /** - * Verify the request by reviewing the values of client id, redirect uri, client secret, code, etc. + * Verify the request by reviewing the values of OAuth 2.0 parameters. * * @param redirectUri the redirect uri * @param clientId the client id * @param clientSecret the client secret - * @param code the code - * @param grantType the grant type + * @param code the authorization code + * @param grantType the grant type, which must be "authorization_code" * @throws InvalidParameterException with the name of the invalid parameter */ - private void verifyRequest(final String redirectUri, final String clientId, final String clientSecret, - final String code, final String grantType) throws InvalidParameterException { - // clientId is required + private void verifyRequest( + final String redirectUri, + final String clientId, + final String clientSecret, + final String code, + final String grantType + ) throws InvalidParameterException { + if (StringUtils.isBlank(clientId)) { - LOGGER.error("Missing {}", OAuthConstants.CLIENT_ID); + LOGGER.error(OAuthConstants.MISSING_CLIENT_ID_DESCRIPTION); throw new InvalidParameterException(OAuthConstants.CLIENT_ID); } - // clientSecret is required + if (StringUtils.isBlank(clientSecret)) { - LOGGER.error("Missing {}", OAuthConstants.CLIENT_SECRET); + LOGGER.error(OAuthConstants.MISSING_CLIENT_SECRET_DESCRIPTION); throw new InvalidParameterException(OAuthConstants.CLIENT_SECRET); } - // code is required + if (StringUtils.isBlank(code)) { - LOGGER.error("Missing {}", OAuthConstants.CODE); + LOGGER.error(OAuthConstants.MISSING_CODE_DESCRIPTION); throw new InvalidParameterException(OAuthConstants.CODE); } - // redirectUri is required + if (StringUtils.isBlank(redirectUri)) { - LOGGER.error("Missing {}", OAuthConstants.REDIRECT_URI); + LOGGER.error(OAuthConstants.MISSING_REDIRECT_URI_DESCRIPTION); throw new InvalidParameterException(OAuthConstants.REDIRECT_URI); } - // grantType is required + if (StringUtils.isBlank(grantType)) { - LOGGER.error("Missing {}", OAuthConstants.GRANT_TYPE); + LOGGER.error(OAuthConstants.MISSING_GRANT_TYPE_DESCRIPTION); throw new InvalidParameterException(OAuthConstants.GRANT_TYPE); - } - if (!grantType.equalsIgnoreCase(OAuthConstants.AUTHORIZATION_CODE)) { - LOGGER.error("Invalid {} : {}", OAuthConstants.GRANT_TYPE, grantType); + } else if (!grantType.equalsIgnoreCase(OAuthConstants.AUTHORIZATION_CODE)) { + LOGGER.error("{} : {}", OAuthConstants.INVALID_GRANT_TYPE_DESCRIPTION, grantType); throw new InvalidParameterException(OAuthConstants.GRANT_TYPE); } } diff --git a/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/web/OAuth20TokenRefreshTokenController.java b/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/web/OAuth20TokenRefreshTokenController.java index 69bbafa0..b243ec74 100644 --- a/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/web/OAuth20TokenRefreshTokenController.java +++ b/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/web/OAuth20TokenRefreshTokenController.java @@ -19,8 +19,10 @@ package org.jasig.cas.support.oauth.web; import com.fasterxml.jackson.databind.ObjectMapper; + import org.apache.commons.lang3.StringUtils; import org.apache.http.HttpStatus; + import org.jasig.cas.support.oauth.CentralOAuthService; import org.jasig.cas.support.oauth.InvalidParameterException; import org.jasig.cas.support.oauth.OAuthConstants; @@ -28,93 +30,115 @@ import org.jasig.cas.support.oauth.token.AccessToken; import org.jasig.cas.support.oauth.token.InvalidTokenException; import org.jasig.cas.support.oauth.token.RefreshToken; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; + import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.mvc.AbstractController; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; +import java.util.concurrent.TimeUnit; import java.util.HashMap; import java.util.Map; -import java.util.concurrent.TimeUnit; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; /** - * This controller handles requests for grant type refresh token, - * returning an access token which is the CAS service ticket according - * to the service and refresh token (granting ticket) given. + * The OAuth 2.0 refresh access token controller. + * + * This controller handles requests asking for a new access token with "refresh_token" as the grant type. A valid + * refresh token must be provided. The new token is granted by {@link CentralOAuthService#grantOfflineAccessToken} + * using a new service ticket created with the service and ticket granting ticket of the refresh token. * * @author Michael Haselton - * @since 4.1.0 + * @author Longze Chen + * @since 4.1.5 */ public final class OAuth20TokenRefreshTokenController extends AbstractController { + /** Log instance for logging events, info, warnings, errors, etc. */ private static final Logger LOGGER = LoggerFactory.getLogger(OAuth20TokenRefreshTokenController.class); + /** The CAS OAuth authorization service. */ private final CentralOAuthService centralOAuthService; + /** The ticket timeout. */ private final long timeout; /** - * Instantiates a new o auth20 grant type refresh token controller. + * Instantiates a new {@link OAuth20TokenRefreshTokenController}. * - * @param centralOAuthService the central oauth service - * @param timeout the timeout + * @param centralOAuthService the central OAuth service + * @param timeout ticket timeout */ - public OAuth20TokenRefreshTokenController(final CentralOAuthService centralOAuthService, - final long timeout) { + public OAuth20TokenRefreshTokenController( + final CentralOAuthService centralOAuthService, + final long timeout + ) { this.centralOAuthService = centralOAuthService; this.timeout = timeout; } @Override - protected ModelAndView handleRequestInternal(final HttpServletRequest request, final HttpServletResponse response) - throws Exception { + protected ModelAndView handleRequestInternal( + final HttpServletRequest request, + final HttpServletResponse response + ) throws Exception { + + // Verify that all required OAuth parameters are provided. final String refreshTokenId = request.getParameter(OAuthConstants.REFRESH_TOKEN); LOGGER.debug("{} : {}", OAuthConstants.REFRESH_TOKEN, refreshTokenId); - final String clientId = request.getParameter(OAuthConstants.CLIENT_ID); LOGGER.debug("{} : {}", OAuthConstants.CLIENT_ID, clientId); - final String clientSecret = request.getParameter(OAuthConstants.CLIENT_SECRET); LOGGER.debug("{} : {}", OAuthConstants.CLIENT_SECRET, "*********"); - final String grantType = request.getParameter(OAuthConstants.GRANT_TYPE); LOGGER.debug("{} : {}", OAuthConstants.GRANT_TYPE, grantType); - try { verifyRequest(refreshTokenId, clientId, clientSecret, grantType); } catch (final InvalidParameterException e) { - return OAuthUtils.writeJsonError(response, OAuthConstants.INVALID_REQUEST, e.getMessage(), HttpStatus.SC_BAD_REQUEST); + return OAuthUtils.writeJsonError( + response, + OAuthConstants.INVALID_REQUEST, + e.getMessage(), + HttpStatus.SC_BAD_REQUEST + ); } + // Verify that the refresh token is valid. final RefreshToken refreshToken; try { refreshToken = centralOAuthService.getToken(refreshTokenId, RefreshToken.class); } catch (final InvalidTokenException e) { LOGGER.error("Invalid {} : {}", OAuthConstants.REFRESH_TOKEN, refreshTokenId); - return OAuthUtils.writeJsonError(response, OAuthConstants.INVALID_REQUEST, OAuthConstants.INVALID_REFRESH_TOKEN_DESCRIPTION, - HttpStatus.SC_BAD_REQUEST); + return OAuthUtils.writeJsonError( + response, + OAuthConstants.INVALID_REQUEST, + OAuthConstants.INVALID_REFRESH_TOKEN_DESCRIPTION, + HttpStatus.SC_BAD_REQUEST + ); } + // Grant an OFFLINE access token with the given refresh token. final AccessToken accessToken = centralOAuthService.grantOfflineAccessToken(refreshToken); + // Build and return the response. final Map map = new HashMap<>(); - map.put(OAuthConstants.ACCESS_TOKEN, accessToken.getId()); - map.put(OAuthConstants.EXPIRES_IN, - (int) (timeout - TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis() - accessToken.getTicket().getCreationTime()))); map.put(OAuthConstants.TOKEN_TYPE, OAuthConstants.BEARER_TOKEN); - + map.put(OAuthConstants.ACCESS_TOKEN, accessToken.getId()); + final long timeSinceTicketCreation = System.currentTimeMillis() - accessToken.getTicket().getCreationTime(); + final int expiresIn = (int) (timeout - TimeUnit.MILLISECONDS.toSeconds(timeSinceTicketCreation)); + map.put(OAuthConstants.EXPIRES_IN, expiresIn); final ObjectMapper mapper = new ObjectMapper(); final String result = mapper.writeValueAsString(map); LOGGER.debug("result : {}", result); - response.setContentType("application/json"); return OAuthUtils.writeText(response, result, HttpStatus.SC_OK); } /** - * Verify the request by reviewing the values of client id, client secret, refresh token, etc. + * Verify the request by reviewing the values of OAuth 2.0 parameters. * * @param refreshTokenId the refresh token id * @param clientId the client id @@ -122,30 +146,33 @@ protected ModelAndView handleRequestInternal(final HttpServletRequest request, f * @param grantType the grant type * @throws InvalidParameterException with the name of the invalid parameter */ - private void verifyRequest(final String refreshTokenId, final String clientId, final String clientSecret, final String grantType) - throws InvalidParameterException { - // refreshToken is required + private void verifyRequest( + final String refreshTokenId, + final String clientId, + final String clientSecret, + final String grantType + ) throws InvalidParameterException { + if (StringUtils.isBlank(refreshTokenId)) { - LOGGER.error("Missing {}", OAuthConstants.REFRESH_TOKEN); + LOGGER.error(OAuthConstants.MISSING_REFRESH_TOKEN_DESCRIPTION); throw new InvalidParameterException(OAuthConstants.REFRESH_TOKEN); } - // clientId is required + if (StringUtils.isBlank(clientId)) { - LOGGER.error("Missing {}", OAuthConstants.CLIENT_ID); + LOGGER.error(OAuthConstants.MISSING_CLIENT_ID_DESCRIPTION); throw new InvalidParameterException(OAuthConstants.CLIENT_ID); } - // clientSecret is required + if (StringUtils.isBlank(clientSecret)) { - LOGGER.error("Missing {}", OAuthConstants.CLIENT_SECRET); + LOGGER.error(OAuthConstants.MISSING_CLIENT_SECRET_DESCRIPTION); throw new InvalidParameterException(OAuthConstants.CLIENT_SECRET); } - // grantType is required + if (StringUtils.isBlank(grantType)) { - LOGGER.error("Missing {}", OAuthConstants.GRANT_TYPE); + LOGGER.error(OAuthConstants.MISSING_GRANT_TYPE_DESCRIPTION); throw new InvalidParameterException(OAuthConstants.GRANT_TYPE); - } - if (!grantType.equalsIgnoreCase(OAuthConstants.REFRESH_TOKEN)) { - LOGGER.error("Invalid {} : {}", OAuthConstants.GRANT_TYPE, grantType); + } else if (!grantType.equalsIgnoreCase(OAuthConstants.REFRESH_TOKEN)) { + LOGGER.error("{} : {}", OAuthConstants.INVALID_GRANT_TYPE_DESCRIPTION, grantType); throw new InvalidParameterException(OAuthConstants.GRANT_TYPE); } } diff --git a/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/web/OAuth20WrapperController.java b/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/web/OAuth20WrapperController.java index 281f0126..210b11cb 100644 --- a/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/web/OAuth20WrapperController.java +++ b/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/web/OAuth20WrapperController.java @@ -19,63 +19,76 @@ package org.jasig.cas.support.oauth.web; import org.apache.http.HttpStatus; + import org.jasig.cas.CentralAuthenticationService; import org.jasig.cas.authentication.RootCasException; import org.jasig.cas.support.oauth.CentralOAuthService; import org.jasig.cas.support.oauth.InvalidParameterException; import org.jasig.cas.support.oauth.OAuthConstants; import org.jasig.cas.support.oauth.OAuthUtils; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; + import org.springframework.beans.factory.InitializingBean; import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.mvc.AbstractController; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import javax.validation.constraints.NotNull; import java.util.HashMap; import java.util.Map; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.validation.constraints.NotNull; /** - * This controller is the main entry point for OAuth version 2.0 - * wrapping in CAS, should be mapped to something like /oauth2.0/*. Dispatch - * request to specific controllers : authorize, accessToken... + * The OAuth 2.0 wrapper controller. + * + * This controller is the main entry point for OAuth 2.0 wrapping in CAS. With current CAS settings, it is mapped to + * {@literal /oauth2/*} with {@literal /login} as the login URL. Requests are dispatched to specific + * controllers based on {@literal /*}. * * @author Jerome Leleu * @author Michael Haselton - * @since 3.5.0 + * @author Longze Chen + * @since 4.1.5 */ public final class OAuth20WrapperController extends BaseOAuthWrapperController implements InitializingBean { + /** Log instance for logging events, info, warnings, errors, etc. */ private static final Logger LOGGER = LoggerFactory.getLogger(OAuth20WrapperController.class); + /** Authorization controllers. */ private AbstractController authorizeController; private AbstractController authorizeCallbackController; private AbstractController authorizeCallbackActionController; + /** Token controllers. */ private AbstractController tokenAuthorizationCodeController; private AbstractController tokenRefreshTokenController; + /** Revoke controllers. */ private AbstractController revokeTokenController; private AbstractController revokeClientPrincipalTokensController; private AbstractController revokeClientTokensController; + /** Profile controllers. */ private AbstractController profileController; + /** Metadata controllers. */ private AbstractController metadataPrincipalController; private AbstractController metadataClientController; - /** Instance of CentralAuthenticationService. */ + /** The CAS primary authentication service. */ @NotNull private CentralAuthenticationService centralAuthenticationService; - /** Instance of CentralOAuthService. */ + /** The CAS OAuth authorization service. */ @NotNull private CentralOAuthService centralOAuthService; @Override public void afterPropertiesSet() throws Exception { + authorizeController = new OAuth20AuthorizeController(centralOAuthService, loginUrl); authorizeCallbackController = new OAuth20AuthorizeCallbackController(centralOAuthService, ticketRegistry); authorizeCallbackActionController = new OAuth20AuthorizeCallbackActionController(centralOAuthService, timeout); @@ -94,11 +107,15 @@ public void afterPropertiesSet() throws Exception { } @Override - public ModelAndView handleRequest(final HttpServletRequest request, final HttpServletResponse response) throws Exception { + public ModelAndView handleRequest( + final HttpServletRequest request, + final HttpServletResponse response + ) throws Exception { + try { return super.handleRequest(request, response); } catch (final RootCasException e) { - // capture any root cas exceptions and display them properly to the user. + // Capture any root CAS exceptions and display them properly to the user. final Map map = new HashMap<>(); map.put("rootCauseException", e); return new ModelAndView(OAuthConstants.ERROR_VIEW, map); @@ -106,67 +123,83 @@ public ModelAndView handleRequest(final HttpServletRequest request, final HttpSe } @Override - protected ModelAndView internalHandleRequest(final String method, final HttpServletRequest request, - final HttpServletResponse response) throws Exception { - // authorize + protected ModelAndView internalHandleRequest( + final String method, + final HttpServletRequest request, + final HttpServletResponse response + ) throws Exception { + + // Authorize if (OAuthConstants.AUTHORIZE_URL.equals(method) && "GET".equals(request.getMethod())) { return authorizeController.handleRequest(request, response); } - // authorize callback + // Authorize Callback if (OAuthConstants.CALLBACK_AUTHORIZE_URL.equals(method) && "GET".equals(request.getMethod())) { return authorizeCallbackController.handleRequest(request, response); } - // authorize callback action + // Authorize Callback Action if (OAuthConstants.CALLBACK_AUTHORIZE_ACTION_URL.equals(method) && "GET".equals(request.getMethod())) { return authorizeCallbackActionController.handleRequest(request, response); } - // token + // Token (2 controllers) if (OAuthConstants.TOKEN_URL.equals(method) && "POST".equals(request.getMethod())) { final String grantType = request.getParameter(OAuthConstants.GRANT_TYPE); LOGGER.debug("{} : {}", OAuthConstants.GRANT_TYPE, grantType); - if (grantType != null) { if (grantType.equals(OAuthConstants.AUTHORIZATION_CODE)) { + // Token 1: Exchange authorization code for ONLINE access token and OFFLINE refresh token return tokenAuthorizationCodeController.handleRequest(request, response); } else if (grantType.equals(OAuthConstants.REFRESH_TOKEN)) { + // Token 2: Refresh access token using refresh token return tokenRefreshTokenController.handleRequest(request, response); } } - - return OAuthUtils.writeJsonError(response, OAuthConstants.INVALID_REQUEST, - new InvalidParameterException(OAuthConstants.GRANT_TYPE).getMessage(), - HttpStatus.SC_BAD_REQUEST); + // Missing or invalid grant type + return OAuthUtils.writeJsonError( + response, + OAuthConstants.INVALID_REQUEST, + new InvalidParameterException(OAuthConstants.GRANT_TYPE).getMessage(), + HttpStatus.SC_BAD_REQUEST); } - // revoke + // Revoke (3 controllers) if (OAuthConstants.REVOKE_URL.equals(method) && "POST".equals(request.getMethod())) { if (request.getParameterMap().containsKey(OAuthConstants.CLIENT_ID)) { + // Revoke 1: Revoke all client tokens of a client if both client id and secret are provided if (request.getParameterMap().containsKey(OAuthConstants.CLIENT_SECRET)) { return revokeClientTokensController.handleRequest(request, response); } + // Revoke 2: Revoke all tokens of a given client for a given principal if the client id is provided + // without a client secret. In addition, a valid access token of type CAS, ONLINE or OFFLINE must be + // presented for authorization. If of type CAS, any client id can be specified; if ONLINE or OFFLINE, + // only the client id associated with the access token will work. return revokeClientPrincipalTokensController.handleRequest(request, response); } else { + // Revoke 3: Revoke a token if neither the client id nor secret is provided. A token must be presented. return revokeTokenController.handleRequest(request, response); } } - // profile + // Profile: Retrieve the profile (i.e. identifier and attributes) of a user associated with an access token. if (OAuthConstants.PROFILE_URL.equals(method) && "GET".equals(request.getMethod())) { return profileController.handleRequest(request, response); } - // metadata + // Metadata (2 controllers) if (OAuthConstants.METADATA_URL.equals(method) && "POST".equals(request.getMethod())) { if (request.getParameterMap().containsKey(OAuthConstants.CLIENT_ID) && request.getParameterMap().containsKey(OAuthConstants.CLIENT_SECRET)) { + // Metadata 1: Retrieve metadata about a given client if both the client id and secret are provided return metadataClientController.handleRequest(request, response); } else { + // Metadata 2: Retrieve metadata about a principal if neither the client id nor the secret is provided. + // In addition, a valid access token of type CAS must be presented for authorization. return metadataPrincipalController.handleRequest(request, response); } } - // error + // Unknown OAuth 2.0 endpoint and/or unsupported HTTP method LOGGER.error("Unknown method : {}", method); OAuthUtils.writeTextError(response, OAuthConstants.INVALID_REQUEST, HttpStatus.SC_BAD_REQUEST); return null; diff --git a/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/web/view/OAuthCas30ResponseView.java b/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/web/view/OAuthCas30ResponseView.java index 8eff9cc4..c6198f78 100644 --- a/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/web/view/OAuthCas30ResponseView.java +++ b/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/web/view/OAuthCas30ResponseView.java @@ -19,29 +19,38 @@ package org.jasig.cas.support.oauth.web.view; import org.apache.commons.lang3.StringUtils; -import org.jasig.cas.CasProtocolConstants; + import org.jasig.cas.authentication.support.CasAttributeEncoder; +import org.jasig.cas.CasProtocolConstants; import org.jasig.cas.services.ServicesManager; import org.jasig.cas.support.oauth.OAuthConstants; import org.jasig.cas.web.view.Cas30ResponseView; + import org.springframework.web.servlet.view.AbstractUrlBasedView; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import javax.validation.constraints.NotNull; import java.util.HashMap; import java.util.Map; import java.util.Set; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.validation.constraints.NotNull; + /** - * Appends the OAuth Access Token to the attributes if found. + * The OAuth CAS 3.0 (protocol version as opposed to the implementation level) view. + * + * This class expand the {@link Cas30ResponseView} by appending the CAS access token to the attributes if found. With + * current CAS settings, this class replaces {@link Cas30ResponseView} and serves as the success view for the protocol + * 3.0 service validation controller. * * @author Michael Haselton - * @since 4.1.0 + * @author Longze Chen + * @since 4.1.5 */ public class OAuthCas30ResponseView extends Cas30ResponseView { + /** - * Instantiates a new Abstract cas response view. + * Instantiates a new {@link OAuthCas30ResponseView}. * * @param view the view */ @@ -51,36 +60,36 @@ protected OAuthCas30ResponseView(final AbstractUrlBasedView view) { @Override @SuppressWarnings("unchecked") - protected void prepareMergedOutputModel(final Map model, final HttpServletRequest request, - final HttpServletResponse response) throws Exception { + protected void prepareMergedOutputModel( + final Map model, + final HttpServletRequest request, + final HttpServletResponse response + ) throws Exception { super.prepareMergedOutputModel(model, request, response); - - final HashMap attributes = (HashMap) model.get(CasProtocolConstants.VALIDATION_CAS_MODEL_ATTRIBUTE_NAME_ATTRIBUTES); + final HashMap attributes + = (HashMap) model.get(CasProtocolConstants.VALIDATION_CAS_MODEL_ATTRIBUTE_NAME_ATTRIBUTES); final String accessToken = (String) model.get(OAuthConstants.CAS_PROTOCOL_ACCESS_TOKEN); - if (accessToken != null) { - attributes.put(OAuthConstants.CAS_PROTOCOL_ACCESS_TOKEN, accessToken); - } final Set accessTokenScope = (Set) model.get(OAuthConstants.CAS_PROTOCOL_ACCESS_TOKEN_SCOPE); + final String scopes = StringUtils.join(accessTokenScope, " "); if (accessToken != null) { - attributes.put(OAuthConstants.CAS_PROTOCOL_ACCESS_TOKEN_SCOPE, StringUtils.join(accessTokenScope, " ")); + attributes.put(OAuthConstants.CAS_PROTOCOL_ACCESS_TOKEN, accessToken); + attributes.put(OAuthConstants.CAS_PROTOCOL_ACCESS_TOKEN_SCOPE, scopes); } } /** - * Sets services manager. + * Set the services manager. * - * @param servicesManager the services manager - * @since 4.1 + * @param servicesManager the services manager to set */ public void setServicesManager(@NotNull final ServicesManager servicesManager) { super.setServicesManager(servicesManager); } /** - * Sets cas attribute encoder. + * Set the CAS attribute encoder. * - * @param casAttributeEncoder the cas attribute encoder - * @since 4.1 + * @param casAttributeEncoder the CAS attribute encoder to set */ public void setCasAttributeEncoder(@NotNull final CasAttributeEncoder casAttributeEncoder) { super.setCasAttributeEncoder(casAttributeEncoder); diff --git a/cas-server-support-oauth/src/test/java/org/jasig/cas/support/oauth/OAuthTestSuite.java b/cas-server-support-oauth/src/test/java/org/jasig/cas/support/oauth/OAuthTestSuite.java index 28bd72ce..3a9ad196 100644 --- a/cas-server-support-oauth/src/test/java/org/jasig/cas/support/oauth/OAuthTestSuite.java +++ b/cas-server-support-oauth/src/test/java/org/jasig/cas/support/oauth/OAuthTestSuite.java @@ -18,21 +18,44 @@ */ package org.jasig.cas.support.oauth; +import org.jasig.cas.support.oauth.web.OAuth20AuthorizeCallbackActionControllerTests; +import org.jasig.cas.support.oauth.web.OAuth20MetadataClientControllerTests; +import org.jasig.cas.support.oauth.web.OAuth20MetadataPrincipalControllerTests; +import org.jasig.cas.support.oauth.web.OAuth20RevokeClientPrincipalTokensControllerTests; +import org.jasig.cas.support.oauth.web.OAuth20RevokeClientTokensControllerTests; +import org.jasig.cas.support.oauth.web.OAuth20RevokeTokenControllerTests; +import org.jasig.cas.support.oauth.web.OAuth20ServiceValidateControllerTests; import org.jasig.cas.support.oauth.web.OAuth20TokenAuthorizationCodeControllerTests; import org.jasig.cas.support.oauth.web.OAuth20AuthorizeControllerTests; import org.jasig.cas.support.oauth.web.OAuth20AuthorizeCallbackControllerTests; import org.jasig.cas.support.oauth.web.OAuth20ProfileControllerTests; +import org.jasig.cas.support.oauth.web.OAuth20TokenRefreshTokenControllerTests; import org.jasig.cas.support.oauth.web.OAuth20WrapperControllerTests; + import org.junit.runner.RunWith; import org.junit.runners.Suite; -@RunWith(Suite.class) -@Suite.SuiteClasses({OAuth20TokenAuthorizationCodeControllerTests.class, OAuth20AuthorizeControllerTests.class, - OAuth20AuthorizeCallbackControllerTests.class, OAuth20ProfileControllerTests.class, - OAuth20WrapperControllerTests.class}) /** * OAuth test suite that runs all test in a batch. + * * @author Misagh Moayyed - * @since 4.0.0 + * @author Longze Chen + * @since 4.1.5 */ +@RunWith(Suite.class) +@Suite.SuiteClasses({ + OAuth20WrapperControllerTests.class, + OAuth20AuthorizeControllerTests.class, + OAuth20AuthorizeCallbackControllerTests.class, + OAuth20AuthorizeCallbackActionControllerTests.class, + OAuth20TokenRefreshTokenControllerTests.class, + OAuth20TokenAuthorizationCodeControllerTests.class, + OAuth20ProfileControllerTests.class, + OAuth20MetadataClientControllerTests.class, + OAuth20MetadataPrincipalControllerTests.class, + OAuth20RevokeTokenControllerTests.class, + OAuth20RevokeClientTokensControllerTests.class, + OAuth20RevokeClientPrincipalTokensControllerTests.class, + OAuth20ServiceValidateControllerTests.class, +}) public class OAuthTestSuite {} diff --git a/cas-server-support-oauth/src/test/java/org/jasig/cas/support/oauth/web/OAuth20AuthorizeCallbackActionControllerTests.java b/cas-server-support-oauth/src/test/java/org/jasig/cas/support/oauth/web/OAuth20AuthorizeCallbackActionControllerTests.java index b38727f3..519331b2 100644 --- a/cas-server-support-oauth/src/test/java/org/jasig/cas/support/oauth/web/OAuth20AuthorizeCallbackActionControllerTests.java +++ b/cas-server-support-oauth/src/test/java/org/jasig/cas/support/oauth/web/OAuth20AuthorizeCallbackActionControllerTests.java @@ -18,14 +18,21 @@ */ package org.jasig.cas.support.oauth.web; - import org.jasig.cas.support.oauth.CentralOAuthService; import org.jasig.cas.support.oauth.OAuthConstants; import org.jasig.cas.support.oauth.token.AccessToken; import org.jasig.cas.support.oauth.token.AuthorizationCode; import org.jasig.cas.support.oauth.token.TokenType; import org.jasig.cas.ticket.TicketGrantingTicket; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; import org.junit.Test; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.mock.web.MockHttpSession; @@ -36,20 +43,15 @@ import java.util.HashSet; import java.util.Set; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; /** * This class tests the {@link OAuth20AuthorizeCallbackActionController} class. * * @author Fitz Elliott - * @since 3.5.2 + * @author Longze Chen + * @since 4.1.5 */ - public final class OAuth20AuthorizeCallbackActionControllerTests { private static final String CONTEXT = "/oauth2.0/"; @@ -66,8 +68,6 @@ public final class OAuth20AuthorizeCallbackActionControllerTests { private static final String SCOPE = NAME1 + " " + NAME2; - private static final String SERVICE_NAME = "serviceName"; - private static final String CLIENT_ID = "client1"; private static final String RESPONSE_TYPE = "token"; @@ -80,8 +80,9 @@ public final class OAuth20AuthorizeCallbackActionControllerTests { @Test public void verifyActionDenied() throws Exception { - final MockHttpServletRequest mockRequest = new MockHttpServletRequest( - "GET", CONTEXT + OAuthConstants.CALLBACK_AUTHORIZE_ACTION_URL); + + final MockHttpServletRequest mockRequest + = new MockHttpServletRequest("GET", CONTEXT + OAuthConstants.CALLBACK_AUTHORIZE_ACTION_URL); final MockHttpSession mockSession = new MockHttpSession(); mockSession.putValue(OAuthConstants.OAUTH20_RESPONSE_TYPE, RESPONSE_TYPE); mockSession.putValue(OAuthConstants.OAUTH20_CLIENT_ID, CLIENT_ID); @@ -101,7 +102,9 @@ public void verifyActionDenied() throws Exception { final ModelAndView modelAndView = oauth20WrapperController.handleRequest(mockRequest, mockResponse); assertTrue(modelAndView.getView() instanceof RedirectView); final RedirectView redirectView = (RedirectView) modelAndView.getView(); - assertTrue(redirectView.getUrl().endsWith(REDIRECT_URI + "?" + OAuthConstants.ERROR + "=" + OAuthConstants.ACCESS_DENIED)); + assertTrue(redirectView.getUrl().endsWith( + REDIRECT_URI + "?" + OAuthConstants.ERROR + "=" + OAuthConstants.ACCESS_DENIED + )); assertNull(mockSession.getAttribute(OAuthConstants.OAUTH20_RESPONSE_TYPE)); assertNull(mockSession.getAttribute(OAuthConstants.OAUTH20_CLIENT_ID)); @@ -114,8 +117,9 @@ public void verifyActionDenied() throws Exception { @Test public void verifyNoClientIdError() throws Exception { - final MockHttpServletRequest mockRequest = new MockHttpServletRequest( - "GET", CONTEXT + OAuthConstants.CALLBACK_AUTHORIZE_ACTION_URL); + + final MockHttpServletRequest mockRequest + = new MockHttpServletRequest("GET", CONTEXT + OAuthConstants.CALLBACK_AUTHORIZE_ACTION_URL); final MockHttpSession mockSession = new MockHttpSession(); mockSession.putValue(OAuthConstants.OAUTH20_RESPONSE_TYPE, RESPONSE_TYPE); mockSession.putValue(OAuthConstants.OAUTH20_STATE, STATE); @@ -124,8 +128,10 @@ public void verifyNoClientIdError() throws Exception { mockSession.putValue(OAuthConstants.OAUTH20_LOGIN_TICKET_ID, TICKET_GRANTING_TICKET_ID); mockSession.putValue(OAuthConstants.OAUTH20_SCOPE, SCOPE); mockRequest.setSession(mockSession); - mockRequest.setParameter(OAuthConstants.OAUTH20_APPROVAL_PROMPT_ACTION, - OAuthConstants.OAUTH20_APPROVAL_PROMPT_ACTION_ALLOW); + mockRequest.setParameter( + OAuthConstants.OAUTH20_APPROVAL_PROMPT_ACTION, + OAuthConstants.OAUTH20_APPROVAL_PROMPT_ACTION_ALLOW + ); final MockHttpServletResponse mockResponse = new MockHttpServletResponse(); @@ -146,8 +152,9 @@ public void verifyNoClientIdError() throws Exception { @Test public void verifyNoRedirectError() throws Exception { - final MockHttpServletRequest mockRequest = new MockHttpServletRequest( - "GET", CONTEXT + OAuthConstants.CALLBACK_AUTHORIZE_ACTION_URL); + + final MockHttpServletRequest mockRequest + = new MockHttpServletRequest("GET", CONTEXT + OAuthConstants.CALLBACK_AUTHORIZE_ACTION_URL); final MockHttpSession mockSession = new MockHttpSession(); mockSession.putValue(OAuthConstants.OAUTH20_RESPONSE_TYPE, RESPONSE_TYPE); mockSession.putValue(OAuthConstants.OAUTH20_STATE, STATE); @@ -156,8 +163,10 @@ public void verifyNoRedirectError() throws Exception { mockSession.putValue(OAuthConstants.OAUTH20_LOGIN_TICKET_ID, TICKET_GRANTING_TICKET_ID); mockSession.putValue(OAuthConstants.OAUTH20_SCOPE, SCOPE); mockRequest.setSession(mockSession); - mockRequest.setParameter(OAuthConstants.OAUTH20_APPROVAL_PROMPT_ACTION, - OAuthConstants.OAUTH20_APPROVAL_PROMPT_ACTION_ALLOW); + mockRequest.setParameter( + OAuthConstants.OAUTH20_APPROVAL_PROMPT_ACTION, + OAuthConstants.OAUTH20_APPROVAL_PROMPT_ACTION_ALLOW + ); final MockHttpServletResponse mockResponse = new MockHttpServletResponse(); @@ -179,6 +188,7 @@ public void verifyNoRedirectError() throws Exception { @Test public void verifyResponseIsTokenWithState() throws Exception { + final AuthorizationCode authorizationCode = mock(AuthorizationCode.class); final TicketGrantingTicket ticketGrantingTicket = mock(TicketGrantingTicket.class); @@ -193,12 +203,16 @@ public void verifyResponseIsTokenWithState() throws Exception { scopes.add(NAME2); final CentralOAuthService centralOAuthService = mock(CentralOAuthService.class); - when(centralOAuthService.grantAuthorizationCode(TokenType.ONLINE, CLIENT_ID, TICKET_GRANTING_TICKET_ID, REDIRECT_URI, scopes)) - .thenReturn(authorizationCode); + when(centralOAuthService.grantAuthorizationCode( + TokenType.ONLINE, CLIENT_ID, + TICKET_GRANTING_TICKET_ID, + REDIRECT_URI, + scopes + )).thenReturn(authorizationCode); when(centralOAuthService.grantOnlineAccessToken(authorizationCode)).thenReturn(accessToken); - final MockHttpServletRequest mockRequest = new MockHttpServletRequest( - "GET", CONTEXT + OAuthConstants.CALLBACK_AUTHORIZE_ACTION_URL); + final MockHttpServletRequest mockRequest + = new MockHttpServletRequest("GET", CONTEXT + OAuthConstants.CALLBACK_AUTHORIZE_ACTION_URL); final MockHttpSession mockSession = new MockHttpSession(); mockSession.putValue(OAuthConstants.OAUTH20_RESPONSE_TYPE, RESPONSE_TYPE); mockSession.putValue(OAuthConstants.OAUTH20_CLIENT_ID, CLIENT_ID); @@ -207,8 +221,10 @@ public void verifyResponseIsTokenWithState() throws Exception { mockSession.putValue(OAuthConstants.OAUTH20_LOGIN_TICKET_ID, TICKET_GRANTING_TICKET_ID); mockSession.putValue(OAuthConstants.OAUTH20_SCOPE_SET, scopes); mockRequest.setSession(mockSession); - mockRequest.setParameter(OAuthConstants.OAUTH20_APPROVAL_PROMPT_ACTION, - OAuthConstants.OAUTH20_APPROVAL_PROMPT_ACTION_ALLOW); + mockRequest.setParameter( + OAuthConstants.OAUTH20_APPROVAL_PROMPT_ACTION, + OAuthConstants.OAUTH20_APPROVAL_PROMPT_ACTION_ALLOW + ); final MockHttpServletResponse mockResponse = new MockHttpServletResponse(); @@ -220,11 +236,13 @@ public void verifyResponseIsTokenWithState() throws Exception { final ModelAndView modelAndView = oauth20WrapperController.handleRequest(mockRequest, mockResponse); assertTrue(modelAndView.getView() instanceof RedirectView); final RedirectView redirectView = (RedirectView) modelAndView.getView(); - assertEquals(redirectView.getUrl(), - REDIRECT_URI + "#" + OAuthConstants.ACCESS_TOKEN + "=" + accessToken.getId() - + "&" + OAuthConstants.EXPIRES_IN + '=' + TIMEOUT - + "&" + OAuthConstants.TOKEN_TYPE + '=' + OAuthConstants.BEARER_TOKEN - + "&" + OAuthConstants.STATE + '=' + STATE); + assertEquals( + redirectView.getUrl(), + REDIRECT_URI + "#" + OAuthConstants.ACCESS_TOKEN + "=" + accessToken.getId() + + "&" + OAuthConstants.EXPIRES_IN + '=' + TIMEOUT + + "&" + OAuthConstants.TOKEN_TYPE + '=' + OAuthConstants.BEARER_TOKEN + + "&" + OAuthConstants.STATE + '=' + STATE + ); assertNull(mockSession.getAttribute(OAuthConstants.OAUTH20_RESPONSE_TYPE)); assertNull(mockSession.getAttribute(OAuthConstants.OAUTH20_CLIENT_ID)); @@ -237,6 +255,7 @@ public void verifyResponseIsTokenWithState() throws Exception { @Test public void verifyResponseIsTokenWithoutState() throws Exception { + final AuthorizationCode authorizationCode = mock(AuthorizationCode.class); final TicketGrantingTicket ticketGrantingTicket = mock(TicketGrantingTicket.class); @@ -251,12 +270,17 @@ public void verifyResponseIsTokenWithoutState() throws Exception { scopes.add(NAME2); final CentralOAuthService centralOAuthService = mock(CentralOAuthService.class); - when(centralOAuthService.grantAuthorizationCode(TokenType.ONLINE, CLIENT_ID, TICKET_GRANTING_TICKET_ID, REDIRECT_URI, scopes)) - .thenReturn(authorizationCode); + when(centralOAuthService.grantAuthorizationCode( + TokenType.ONLINE, + CLIENT_ID, + TICKET_GRANTING_TICKET_ID, + REDIRECT_URI, + scopes + )).thenReturn(authorizationCode); when(centralOAuthService.grantOnlineAccessToken(authorizationCode)).thenReturn(accessToken); - final MockHttpServletRequest mockRequest = new MockHttpServletRequest( - "GET", CONTEXT + OAuthConstants.CALLBACK_AUTHORIZE_ACTION_URL); + final MockHttpServletRequest mockRequest + = new MockHttpServletRequest("GET", CONTEXT + OAuthConstants.CALLBACK_AUTHORIZE_ACTION_URL); final MockHttpSession mockSession = new MockHttpSession(); mockSession.putValue(OAuthConstants.OAUTH20_RESPONSE_TYPE, RESPONSE_TYPE); mockSession.putValue(OAuthConstants.OAUTH20_CLIENT_ID, CLIENT_ID); @@ -264,8 +288,10 @@ public void verifyResponseIsTokenWithoutState() throws Exception { mockSession.putValue(OAuthConstants.OAUTH20_LOGIN_TICKET_ID, TICKET_GRANTING_TICKET_ID); mockSession.putValue(OAuthConstants.OAUTH20_SCOPE_SET, scopes); mockRequest.setSession(mockSession); - mockRequest.setParameter(OAuthConstants.OAUTH20_APPROVAL_PROMPT_ACTION, - OAuthConstants.OAUTH20_APPROVAL_PROMPT_ACTION_ALLOW); + mockRequest.setParameter( + OAuthConstants.OAUTH20_APPROVAL_PROMPT_ACTION, + OAuthConstants.OAUTH20_APPROVAL_PROMPT_ACTION_ALLOW + ); final MockHttpServletResponse mockResponse = new MockHttpServletResponse(); @@ -277,10 +303,12 @@ public void verifyResponseIsTokenWithoutState() throws Exception { final ModelAndView modelAndView = oauth20WrapperController.handleRequest(mockRequest, mockResponse); assertTrue(modelAndView.getView() instanceof RedirectView); final RedirectView redirectView = (RedirectView) modelAndView.getView(); - assertEquals(redirectView.getUrl(), - REDIRECT_URI + "#" + OAuthConstants.ACCESS_TOKEN + "=" + accessToken.getId() - + "&" + OAuthConstants.EXPIRES_IN + '=' + TIMEOUT - + "&" + OAuthConstants.TOKEN_TYPE + '=' + OAuthConstants.BEARER_TOKEN); + assertEquals( + redirectView.getUrl(), + REDIRECT_URI + "#" + OAuthConstants.ACCESS_TOKEN + "=" + accessToken.getId() + + "&" + OAuthConstants.EXPIRES_IN + '=' + TIMEOUT + + "&" + OAuthConstants.TOKEN_TYPE + '=' + OAuthConstants.BEARER_TOKEN + ); assertNull(mockSession.getAttribute(OAuthConstants.OAUTH20_RESPONSE_TYPE)); assertNull(mockSession.getAttribute(OAuthConstants.OAUTH20_CLIENT_ID)); @@ -293,6 +321,7 @@ public void verifyResponseIsTokenWithoutState() throws Exception { @Test public void verifyResponseIsCodeWithState() throws Exception { + final AuthorizationCode authorizationCode = mock(AuthorizationCode.class); when(authorizationCode.getId()).thenReturn(AC_ID); @@ -301,11 +330,16 @@ public void verifyResponseIsCodeWithState() throws Exception { scopes.add(NAME2); final CentralOAuthService centralOAuthService = mock(CentralOAuthService.class); - when(centralOAuthService.grantAuthorizationCode(TokenType.OFFLINE, CLIENT_ID, TICKET_GRANTING_TICKET_ID, REDIRECT_URI, scopes)) - .thenReturn(authorizationCode); - - final MockHttpServletRequest mockRequest = new MockHttpServletRequest( - "GET", CONTEXT + OAuthConstants.CALLBACK_AUTHORIZE_ACTION_URL); + when(centralOAuthService.grantAuthorizationCode( + TokenType.OFFLINE, + CLIENT_ID, + TICKET_GRANTING_TICKET_ID, + REDIRECT_URI, + scopes + )).thenReturn(authorizationCode); + + final MockHttpServletRequest mockRequest + = new MockHttpServletRequest("GET", CONTEXT + OAuthConstants.CALLBACK_AUTHORIZE_ACTION_URL); final MockHttpSession mockSession = new MockHttpSession(); mockSession.putValue(OAuthConstants.OAUTH20_CLIENT_ID, CLIENT_ID); mockSession.putValue(OAuthConstants.OAUTH20_STATE, STATE); @@ -314,8 +348,10 @@ public void verifyResponseIsCodeWithState() throws Exception { mockSession.putValue(OAuthConstants.OAUTH20_LOGIN_TICKET_ID, TICKET_GRANTING_TICKET_ID); mockSession.putValue(OAuthConstants.OAUTH20_SCOPE_SET, scopes); mockRequest.setSession(mockSession); - mockRequest.setParameter(OAuthConstants.OAUTH20_APPROVAL_PROMPT_ACTION, - OAuthConstants.OAUTH20_APPROVAL_PROMPT_ACTION_ALLOW); + mockRequest.setParameter( + OAuthConstants.OAUTH20_APPROVAL_PROMPT_ACTION, + OAuthConstants.OAUTH20_APPROVAL_PROMPT_ACTION_ALLOW + ); final MockHttpServletResponse mockResponse = new MockHttpServletResponse(); @@ -326,9 +362,11 @@ public void verifyResponseIsCodeWithState() throws Exception { final ModelAndView modelAndView = oauth20WrapperController.handleRequest(mockRequest, mockResponse); assertTrue(modelAndView.getView() instanceof RedirectView); final RedirectView redirectView = (RedirectView) modelAndView.getView(); - assertEquals(redirectView.getUrl(), - REDIRECT_URI + "?" + OAuthConstants.CODE + "=" + AC_ID - + "&" + OAuthConstants.STATE + '=' + STATE); + assertEquals( + redirectView.getUrl(), + REDIRECT_URI + "?" + OAuthConstants.CODE + "=" + AC_ID + + "&" + OAuthConstants.STATE + '=' + STATE + ); assertNull(mockSession.getAttribute(OAuthConstants.OAUTH20_RESPONSE_TYPE)); assertNull(mockSession.getAttribute(OAuthConstants.OAUTH20_CLIENT_ID)); @@ -341,6 +379,7 @@ public void verifyResponseIsCodeWithState() throws Exception { @Test public void verifyResponseIsCodeWithoutState() throws Exception { + final AuthorizationCode authorizationCode = mock(AuthorizationCode.class); when(authorizationCode.getId()).thenReturn(AC_ID); @@ -349,11 +388,16 @@ public void verifyResponseIsCodeWithoutState() throws Exception { scopes.add(NAME2); final CentralOAuthService centralOAuthService = mock(CentralOAuthService.class); - when(centralOAuthService.grantAuthorizationCode(TokenType.OFFLINE, CLIENT_ID, TICKET_GRANTING_TICKET_ID, REDIRECT_URI, scopes)) - .thenReturn(authorizationCode); - - final MockHttpServletRequest mockRequest = new MockHttpServletRequest( - "GET", CONTEXT + OAuthConstants.CALLBACK_AUTHORIZE_ACTION_URL); + when(centralOAuthService.grantAuthorizationCode( + TokenType.OFFLINE, + CLIENT_ID, + TICKET_GRANTING_TICKET_ID, + REDIRECT_URI, + scopes + )).thenReturn(authorizationCode); + + final MockHttpServletRequest mockRequest + = new MockHttpServletRequest("GET", CONTEXT + OAuthConstants.CALLBACK_AUTHORIZE_ACTION_URL); final MockHttpSession mockSession = new MockHttpSession(); mockSession.putValue(OAuthConstants.OAUTH20_CLIENT_ID, CLIENT_ID); mockSession.putValue(OAuthConstants.OAUTH20_REDIRECT_URI, REDIRECT_URI); @@ -361,8 +405,10 @@ public void verifyResponseIsCodeWithoutState() throws Exception { mockSession.putValue(OAuthConstants.OAUTH20_LOGIN_TICKET_ID, TICKET_GRANTING_TICKET_ID); mockSession.putValue(OAuthConstants.OAUTH20_SCOPE_SET, scopes); mockRequest.setSession(mockSession); - mockRequest.setParameter(OAuthConstants.OAUTH20_APPROVAL_PROMPT_ACTION, - OAuthConstants.OAUTH20_APPROVAL_PROMPT_ACTION_ALLOW); + mockRequest.setParameter( + OAuthConstants.OAUTH20_APPROVAL_PROMPT_ACTION, + OAuthConstants.OAUTH20_APPROVAL_PROMPT_ACTION_ALLOW + ); final MockHttpServletResponse mockResponse = new MockHttpServletResponse(); @@ -384,4 +430,3 @@ public void verifyResponseIsCodeWithoutState() throws Exception { assertNull(mockSession.getAttribute(OAuthConstants.OAUTH20_SCOPE_SET)); } } - diff --git a/cas-server-support-oauth/src/test/java/org/jasig/cas/support/oauth/web/OAuth20AuthorizeCallbackControllerTests.java b/cas-server-support-oauth/src/test/java/org/jasig/cas/support/oauth/web/OAuth20AuthorizeCallbackControllerTests.java index 79453dfe..9727729d 100644 --- a/cas-server-support-oauth/src/test/java/org/jasig/cas/support/oauth/web/OAuth20AuthorizeCallbackControllerTests.java +++ b/cas-server-support-oauth/src/test/java/org/jasig/cas/support/oauth/web/OAuth20AuthorizeCallbackControllerTests.java @@ -18,21 +18,26 @@ */ package org.jasig.cas.support.oauth.web; -import static org.junit.Assert.assertEquals; - import org.apache.http.HttpStatus; + import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; + import org.jasig.cas.authentication.Authentication; import org.jasig.cas.authentication.principal.Principal; import org.jasig.cas.support.oauth.CentralOAuthService; import org.jasig.cas.support.oauth.OAuthConstants; import org.jasig.cas.support.oauth.scope.Scope; import org.jasig.cas.support.oauth.token.TokenType; +import org.jasig.cas.ticket.registry.TicketRegistry; import org.jasig.cas.ticket.ServiceTicket; import org.jasig.cas.ticket.TicketGrantingTicket; -import org.jasig.cas.ticket.registry.TicketRegistry; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; import org.junit.Test; + import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.mock.web.MockHttpSession; @@ -42,8 +47,6 @@ import java.util.HashMap; import java.util.Map; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.anySetOf; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -53,7 +56,8 @@ * * @author Jerome Leleu * @author Michael Haselton - * @since 3.5.2 + * @author Longze Chen + * @since 4.1.5 */ public final class OAuth20AuthorizeCallbackControllerTests { @@ -75,12 +79,14 @@ public final class OAuth20AuthorizeCallbackControllerTests { @Test public void verifySetupFailsNoTicket() throws Exception { + final TicketRegistry ticketRegistry = mock(TicketRegistry.class); when(ticketRegistry.getTicket(SERVICE_TICKET_ID)).thenReturn(null); - final MockHttpServletRequest mockRequest = new MockHttpServletRequest( - "GET", CONTEXT + OAuthConstants.CALLBACK_AUTHORIZE_URL); + final MockHttpServletRequest mockRequest + = new MockHttpServletRequest("GET", CONTEXT + OAuthConstants.CALLBACK_AUTHORIZE_URL); mockRequest.setParameter(OAuthConstants.TICKET, SERVICE_TICKET_ID); + final MockHttpServletResponse mockResponse = new MockHttpServletResponse(); final OAuth20WrapperController oauth20WrapperController = new OAuth20WrapperController(); @@ -93,7 +99,6 @@ public void verifySetupFailsNoTicket() throws Exception { assertEquals(CONTENT_TYPE, mockResponse.getContentType()); final ObjectMapper mapper = new ObjectMapper(); - final String expected = "{\"error\":\"" + OAuthConstants.INVALID_GRANT + "\",\"error_description\":\"" + OAuthConstants.EXPIRED_ST_DESCRIPTION + "\"}"; final JsonNode expectedObj = mapper.readTree(expected); @@ -104,15 +109,17 @@ public void verifySetupFailsNoTicket() throws Exception { @Test public void verifySetupFailsExpiredTicket() throws Exception { + final ServiceTicket serviceTicket = mock(ServiceTicket.class); when(serviceTicket.isExpired()).thenReturn(true); final TicketRegistry ticketRegistry = mock(TicketRegistry.class); when(ticketRegistry.getTicket(SERVICE_TICKET_ID)).thenReturn(serviceTicket); - final MockHttpServletRequest mockRequest = new MockHttpServletRequest( - "GET", CONTEXT + OAuthConstants.CALLBACK_AUTHORIZE_URL); + final MockHttpServletRequest mockRequest + = new MockHttpServletRequest("GET", CONTEXT + OAuthConstants.CALLBACK_AUTHORIZE_URL); mockRequest.setParameter(OAuthConstants.TICKET, SERVICE_TICKET_ID); + final MockHttpServletResponse mockResponse = new MockHttpServletResponse(); final OAuth20WrapperController oauth20WrapperController = new OAuth20WrapperController(); @@ -125,9 +132,8 @@ public void verifySetupFailsExpiredTicket() throws Exception { assertEquals(CONTENT_TYPE, mockResponse.getContentType()); final ObjectMapper mapper = new ObjectMapper(); - - final String expected = "{\"error\":\"" + OAuthConstants.INVALID_GRANT + "\",\"error_description\":\"" - + OAuthConstants.EXPIRED_ST_DESCRIPTION + "\"}"; + final String expected = "{\"error\":\"" + OAuthConstants.INVALID_GRANT + + "\",\"error_description\":\"" + OAuthConstants.EXPIRED_ST_DESCRIPTION + "\"}"; final JsonNode expectedObj = mapper.readTree(expected); final JsonNode receivedObj = mapper.readTree(mockResponse.getContentAsString()); assertEquals(expectedObj.get("error").asText(), receivedObj.get("error").asText()); @@ -136,6 +142,7 @@ public void verifySetupFailsExpiredTicket() throws Exception { @Test public void verifySetupOK() throws Exception { + final TicketGrantingTicket ticketGrantingTicket = mock(TicketGrantingTicket.class); when(ticketGrantingTicket.getId()).thenReturn(TICKET_GRANTING_TICKET_ID); @@ -165,11 +172,12 @@ public void verifySetupOK() throws Exception { @Test public void verifyFailIfGrantingTicketNull() throws Exception { + final TicketRegistry ticketRegistry = mock(TicketRegistry.class); when(ticketRegistry.getTicket(TICKET_GRANTING_TICKET_ID)).thenReturn(null); - final MockHttpServletRequest mockRequest = new MockHttpServletRequest( - "GET", CONTEXT + OAuthConstants.CALLBACK_AUTHORIZE_URL); + final MockHttpServletRequest mockRequest + = new MockHttpServletRequest("GET", CONTEXT + OAuthConstants.CALLBACK_AUTHORIZE_URL); final MockHttpSession mockSession = new MockHttpSession(); mockSession.putValue(OAuthConstants.OAUTH20_LOGIN_TICKET_ID, TICKET_GRANTING_TICKET_ID); mockSession.putValue(OAuthConstants.OAUTH20_SCOPE, SCOPE); @@ -190,9 +198,8 @@ public void verifyFailIfGrantingTicketNull() throws Exception { assertEquals(CONTENT_TYPE, mockResponse.getContentType()); final ObjectMapper mapper = new ObjectMapper(); - - final String expected = "{\"error\":\"" + OAuthConstants.INVALID_GRANT + "\",\"error_description\":\"" - + OAuthConstants.EXPIRED_TGT_DESCRIPTION + "\"}"; + final String expected = "{\"error\":\"" + OAuthConstants.INVALID_GRANT + + "\",\"error_description\":\"" + OAuthConstants.EXPIRED_TGT_DESCRIPTION + "\"}"; final JsonNode expectedObj = mapper.readTree(expected); final JsonNode receivedObj = mapper.readTree(mockResponse.getContentAsString()); assertEquals(expectedObj.get("error").asText(), receivedObj.get("error").asText()); @@ -201,14 +208,15 @@ public void verifyFailIfGrantingTicketNull() throws Exception { @Test public void verifyFailIfGrantingTicketExpired() throws Exception { + final TicketGrantingTicket ticketGrantingTicket = mock(TicketGrantingTicket.class); when(ticketGrantingTicket.isExpired()).thenReturn(true); final TicketRegistry ticketRegistry = mock(TicketRegistry.class); when(ticketRegistry.getTicket(TICKET_GRANTING_TICKET_ID)).thenReturn(ticketGrantingTicket); - final MockHttpServletRequest mockRequest = new MockHttpServletRequest( - "GET", CONTEXT + OAuthConstants.CALLBACK_AUTHORIZE_URL); + final MockHttpServletRequest mockRequest + = new MockHttpServletRequest("GET", CONTEXT + OAuthConstants.CALLBACK_AUTHORIZE_URL); final MockHttpSession mockSession = new MockHttpSession(); mockSession.putValue(OAuthConstants.OAUTH20_LOGIN_TICKET_ID, TICKET_GRANTING_TICKET_ID); mockSession.putValue(OAuthConstants.OAUTH20_SCOPE, SCOPE); @@ -229,9 +237,8 @@ public void verifyFailIfGrantingTicketExpired() throws Exception { assertEquals(CONTENT_TYPE, mockResponse.getContentType()); final ObjectMapper mapper = new ObjectMapper(); - - final String expected = "{\"error\":\"" + OAuthConstants.INVALID_GRANT + "\",\"error_description\":\"" - + OAuthConstants.EXPIRED_TGT_DESCRIPTION + "\"}"; + final String expected = "{\"error\":\"" + OAuthConstants.INVALID_GRANT + + "\",\"error_description\":\"" + OAuthConstants.EXPIRED_TGT_DESCRIPTION + "\"}"; final JsonNode expectedObj = mapper.readTree(expected); final JsonNode receivedObj = mapper.readTree(mockResponse.getContentAsString()); assertEquals(expectedObj.get("error").asText(), receivedObj.get("error").asText()); @@ -240,6 +247,7 @@ public void verifyFailIfGrantingTicketExpired() throws Exception { @Test public void verifyBypassPromptIsTrue() throws Exception { + final TicketGrantingTicket ticketGrantingTicket = mock(TicketGrantingTicket.class); when(ticketGrantingTicket.isExpired()).thenReturn(false); @@ -253,8 +261,8 @@ public void verifyBypassPromptIsTrue() throws Exception { final CentralOAuthService centralOAuthService = mock(CentralOAuthService.class); when(centralOAuthService.getScopes(anySetOf(String.class))).thenReturn(scopeMap); - final MockHttpServletRequest mockRequest = new MockHttpServletRequest( - "GET", CONTEXT + OAuthConstants.CALLBACK_AUTHORIZE_URL); + final MockHttpServletRequest mockRequest + = new MockHttpServletRequest("GET", CONTEXT + OAuthConstants.CALLBACK_AUTHORIZE_URL); final MockHttpSession mockSession = new MockHttpSession(); mockSession.putValue(OAuthConstants.OAUTH20_LOGIN_TICKET_ID, TICKET_GRANTING_TICKET_ID); mockSession.putValue(OAuthConstants.OAUTH20_SCOPE, SCOPE); @@ -275,11 +283,13 @@ public void verifyBypassPromptIsTrue() throws Exception { assertTrue(modelAndView.getView() instanceof RedirectView); final RedirectView redirectView = (RedirectView) modelAndView.getView(); assertTrue(redirectView.getUrl().endsWith( - CONTEXT + OAuthConstants.CALLBACK_AUTHORIZE_ACTION_URL + "?action=allow")); + CONTEXT + OAuthConstants.CALLBACK_AUTHORIZE_ACTION_URL + "?action=allow" + )); } @Test public void verifyNoPromptWithExistingToken() throws Exception { + final Principal principal = mock(Principal.class); when(principal.getId()).thenReturn(PRINCIPAL_ID); @@ -302,8 +312,8 @@ public void verifyNoPromptWithExistingToken() throws Exception { when(centralOAuthService.isAccessToken(TokenType.ONLINE, CLIENT_ID, PRINCIPAL_ID, scopeMap.keySet())).thenReturn(true); when(centralOAuthService.isRefreshToken(CLIENT_ID, PRINCIPAL_ID, scopeMap.keySet())).thenReturn(false); - final MockHttpServletRequest mockRequest = new MockHttpServletRequest( - "GET", CONTEXT + OAuthConstants.CALLBACK_AUTHORIZE_URL); + final MockHttpServletRequest mockRequest + = new MockHttpServletRequest("GET", CONTEXT + OAuthConstants.CALLBACK_AUTHORIZE_URL); final MockHttpSession mockSession = new MockHttpSession(); mockSession.putValue(OAuthConstants.OAUTH20_LOGIN_TICKET_ID, TICKET_GRANTING_TICKET_ID); mockSession.putValue(OAuthConstants.OAUTH20_SCOPE, SCOPE); @@ -323,13 +333,15 @@ public void verifyNoPromptWithExistingToken() throws Exception { assertTrue(modelAndView.getView() instanceof RedirectView); final RedirectView redirectView = (RedirectView) modelAndView.getView(); assertTrue(redirectView.getUrl().endsWith( - CONTEXT + OAuthConstants.CALLBACK_AUTHORIZE_ACTION_URL + "?action=allow")); + CONTEXT + OAuthConstants.CALLBACK_AUTHORIZE_ACTION_URL + "?action=allow" + )); assertEquals(scopeMap.keySet(), mockSession.getAttribute(OAuthConstants.OAUTH20_SCOPE_SET)); } @Test public void verifyAutoPromptWithExistingToken() throws Exception { + final Principal principal = mock(Principal.class); when(principal.getId()).thenReturn(PRINCIPAL_ID); @@ -353,8 +365,8 @@ public void verifyAutoPromptWithExistingToken() throws Exception { when(centralOAuthService.isRefreshToken(CLIENT_ID, PRINCIPAL_ID, scopeMap.keySet())).thenReturn(true); - final MockHttpServletRequest mockRequest = new MockHttpServletRequest( - "GET", CONTEXT + OAuthConstants.CALLBACK_AUTHORIZE_URL); + final MockHttpServletRequest mockRequest + = new MockHttpServletRequest("GET", CONTEXT + OAuthConstants.CALLBACK_AUTHORIZE_URL); final MockHttpSession mockSession = new MockHttpSession(); mockSession.putValue(OAuthConstants.OAUTH20_LOGIN_TICKET_ID, TICKET_GRANTING_TICKET_ID); mockSession.putValue(OAuthConstants.OAUTH20_SCOPE, SCOPE); @@ -375,13 +387,15 @@ public void verifyAutoPromptWithExistingToken() throws Exception { assertTrue(modelAndView.getView() instanceof RedirectView); final RedirectView redirectView = (RedirectView) modelAndView.getView(); assertTrue(redirectView.getUrl().endsWith( - CONTEXT + OAuthConstants.CALLBACK_AUTHORIZE_ACTION_URL + "?action=allow")); + CONTEXT + OAuthConstants.CALLBACK_AUTHORIZE_ACTION_URL + "?action=allow" + )); assertEquals(scopeMap.keySet(), mockSession.getAttribute(OAuthConstants.OAUTH20_SCOPE_SET)); } @Test public void verifyOK() throws Exception { + final Principal principal = mock(Principal.class); when(principal.getId()).thenReturn(PRINCIPAL_ID); @@ -402,8 +416,8 @@ public void verifyOK() throws Exception { final CentralOAuthService centralOAuthService = mock(CentralOAuthService.class); when(centralOAuthService.getScopes(anySetOf(String.class))).thenReturn(scopeMap); - final MockHttpServletRequest mockRequest = new MockHttpServletRequest( - "GET", CONTEXT + OAuthConstants.CALLBACK_AUTHORIZE_URL); + final MockHttpServletRequest mockRequest + = new MockHttpServletRequest("GET", CONTEXT + OAuthConstants.CALLBACK_AUTHORIZE_URL); final MockHttpSession mockSession = new MockHttpSession(); mockSession.putValue(OAuthConstants.OAUTH20_LOGIN_TICKET_ID, TICKET_GRANTING_TICKET_ID); mockSession.putValue(OAuthConstants.OAUTH20_SCOPE, SCOPE); @@ -431,6 +445,7 @@ public void verifyOK() throws Exception { @Test public void verifyOKWhenBypassApprovalFalse() throws Exception { + final Principal principal = mock(Principal.class); when(principal.getId()).thenReturn(PRINCIPAL_ID); @@ -451,8 +466,8 @@ public void verifyOKWhenBypassApprovalFalse() throws Exception { final CentralOAuthService centralOAuthService = mock(CentralOAuthService.class); when(centralOAuthService.getScopes(anySetOf(String.class))).thenReturn(scopeMap); - final MockHttpServletRequest mockRequest = new MockHttpServletRequest( - "GET", CONTEXT + OAuthConstants.CALLBACK_AUTHORIZE_URL); + final MockHttpServletRequest mockRequest + = new MockHttpServletRequest("GET", CONTEXT + OAuthConstants.CALLBACK_AUTHORIZE_URL); final MockHttpSession mockSession = new MockHttpSession(); mockSession.putValue(OAuthConstants.OAUTH20_LOGIN_TICKET_ID, TICKET_GRANTING_TICKET_ID); mockSession.putValue(OAuthConstants.OAUTH20_SCOPE, SCOPE); @@ -481,6 +496,7 @@ public void verifyOKWhenBypassApprovalFalse() throws Exception { @Test public void verifyNoPromptWithoutExistingToken() throws Exception { + final Principal principal = mock(Principal.class); when(principal.getId()).thenReturn(PRINCIPAL_ID); @@ -500,12 +516,16 @@ public void verifyNoPromptWithoutExistingToken() throws Exception { final CentralOAuthService centralOAuthService = mock(CentralOAuthService.class); when(centralOAuthService.getScopes(anySetOf(String.class))).thenReturn(scopeMap); - when(centralOAuthService.isAccessToken(TokenType.ONLINE, CLIENT_ID, PRINCIPAL_ID, scopeMap.keySet())).thenReturn(false); + when(centralOAuthService.isAccessToken( + TokenType.ONLINE, + CLIENT_ID, + PRINCIPAL_ID, + scopeMap.keySet() + )).thenReturn(false); when(centralOAuthService.isRefreshToken(CLIENT_ID, PRINCIPAL_ID, scopeMap.keySet())).thenReturn(true); - - final MockHttpServletRequest mockRequest = new MockHttpServletRequest( - "GET", CONTEXT + OAuthConstants.CALLBACK_AUTHORIZE_URL); + final MockHttpServletRequest mockRequest + = new MockHttpServletRequest("GET", CONTEXT + OAuthConstants.CALLBACK_AUTHORIZE_URL); final MockHttpSession mockSession = new MockHttpSession(); mockSession.putValue(OAuthConstants.OAUTH20_LOGIN_TICKET_ID, TICKET_GRANTING_TICKET_ID); mockSession.putValue(OAuthConstants.OAUTH20_SCOPE, SCOPE); @@ -533,6 +553,7 @@ public void verifyNoPromptWithoutExistingToken() throws Exception { @Test public void verifyAutoPromptWithoutExistingToken() throws Exception { + final Principal principal = mock(Principal.class); when(principal.getId()).thenReturn(PRINCIPAL_ID); @@ -552,12 +573,16 @@ public void verifyAutoPromptWithoutExistingToken() throws Exception { final CentralOAuthService centralOAuthService = mock(CentralOAuthService.class); when(centralOAuthService.getScopes(anySetOf(String.class))).thenReturn(scopeMap); - when(centralOAuthService.isAccessToken(TokenType.ONLINE, CLIENT_ID, PRINCIPAL_ID, scopeMap.keySet())).thenReturn(true); + when(centralOAuthService.isAccessToken( + TokenType.ONLINE, + CLIENT_ID, + PRINCIPAL_ID, + scopeMap.keySet() + )).thenReturn(true); when(centralOAuthService.isRefreshToken(CLIENT_ID, PRINCIPAL_ID, scopeMap.keySet())).thenReturn(false); - - final MockHttpServletRequest mockRequest = new MockHttpServletRequest( - "GET", CONTEXT + OAuthConstants.CALLBACK_AUTHORIZE_URL); + final MockHttpServletRequest mockRequest + = new MockHttpServletRequest("GET", CONTEXT + OAuthConstants.CALLBACK_AUTHORIZE_URL); final MockHttpSession mockSession = new MockHttpSession(); mockSession.putValue(OAuthConstants.OAUTH20_LOGIN_TICKET_ID, TICKET_GRANTING_TICKET_ID); mockSession.putValue(OAuthConstants.OAUTH20_SCOPE, SCOPE); @@ -583,5 +608,4 @@ public void verifyAutoPromptWithoutExistingToken() throws Exception { assertEquals(scopeMap.keySet(), mockSession.getAttribute(OAuthConstants.OAUTH20_SCOPE_SET)); } - } diff --git a/cas-server-support-oauth/src/test/java/org/jasig/cas/support/oauth/web/OAuth20AuthorizeControllerTests.java b/cas-server-support-oauth/src/test/java/org/jasig/cas/support/oauth/web/OAuth20AuthorizeControllerTests.java index 8f6a07f0..fa167378 100644 --- a/cas-server-support-oauth/src/test/java/org/jasig/cas/support/oauth/web/OAuth20AuthorizeControllerTests.java +++ b/cas-server-support-oauth/src/test/java/org/jasig/cas/support/oauth/web/OAuth20AuthorizeControllerTests.java @@ -22,7 +22,14 @@ import org.jasig.cas.support.oauth.OAuthConstants; import org.jasig.cas.support.oauth.services.OAuthRegisteredService; import org.jasig.cas.support.oauth.token.TokenType; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; import org.junit.Test; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.web.servlet.ModelAndView; @@ -30,12 +37,6 @@ import javax.servlet.http.HttpSession; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; - /** * This class tests the {@link OAuth20AuthorizeController} class. * @@ -90,23 +91,28 @@ public final class OAuth20AuthorizeControllerTests { @Test public void verifyInvalidResponseType() throws Exception { - final MockHttpServletRequest mockRequest = new MockHttpServletRequest("GET", CONTEXT - + OAuthConstants.AUTHORIZE_URL); + + final MockHttpServletRequest mockRequest + = new MockHttpServletRequest("GET", CONTEXT + OAuthConstants.AUTHORIZE_URL); mockRequest.setParameter(OAuthConstants.RESPONSE_TYPE, INVALID_RESPONSE_TYPE); mockRequest.setParameter(OAuthConstants.CLIENT_ID, CLIENT_ID); mockRequest.setParameter(OAuthConstants.REDIRECT_URI, REDIRECT_URI); mockRequest.setParameter(OAuthConstants.ACCESS_TYPE, ACCESS_TYPE); + final MockHttpServletResponse mockResponse = new MockHttpServletResponse(); + final OAuth20WrapperController oauth20WrapperController = new OAuth20WrapperController(); oauth20WrapperController.afterPropertiesSet(); + final ModelAndView modelAndView = oauth20WrapperController.handleRequest(mockRequest, mockResponse); assertEquals(OAuthConstants.ERROR_VIEW, modelAndView.getViewName()); } @Test public void verifyNoClientId() throws Exception { - final MockHttpServletRequest mockRequest = new MockHttpServletRequest("GET", CONTEXT - + OAuthConstants.AUTHORIZE_URL); + + final MockHttpServletRequest mockRequest + = new MockHttpServletRequest("GET", CONTEXT + OAuthConstants.AUTHORIZE_URL); mockRequest.setParameter(OAuthConstants.RESPONSE_TYPE, RESPONSE_TYPE); mockRequest.setParameter(OAuthConstants.REDIRECT_URI, REDIRECT_URI); mockRequest.setParameter(OAuthConstants.ACCESS_TYPE, ACCESS_TYPE); @@ -122,8 +128,9 @@ public void verifyNoClientId() throws Exception { @Test public void verifyNoRedirectUri() throws Exception { - final MockHttpServletRequest mockRequest = new MockHttpServletRequest("GET", CONTEXT - + OAuthConstants.AUTHORIZE_URL); + + final MockHttpServletRequest mockRequest + = new MockHttpServletRequest("GET", CONTEXT + OAuthConstants.AUTHORIZE_URL); mockRequest.setParameter(OAuthConstants.RESPONSE_TYPE, RESPONSE_TYPE); mockRequest.setParameter(OAuthConstants.CLIENT_ID, CLIENT_ID); mockRequest.setParameter(OAuthConstants.ACCESS_TYPE, ACCESS_TYPE); @@ -139,8 +146,9 @@ public void verifyNoRedirectUri() throws Exception { @Test public void verifyInvalidAccessType() throws Exception { - final MockHttpServletRequest mockRequest = new MockHttpServletRequest("GET", CONTEXT - + OAuthConstants.AUTHORIZE_URL); + + final MockHttpServletRequest mockRequest + = new MockHttpServletRequest("GET", CONTEXT + OAuthConstants.AUTHORIZE_URL); mockRequest.setParameter(OAuthConstants.RESPONSE_TYPE, RESPONSE_TYPE); mockRequest.setParameter(OAuthConstants.CLIENT_ID, CLIENT_ID); mockRequest.setParameter(OAuthConstants.REDIRECT_URI, REDIRECT_URI); @@ -157,8 +165,9 @@ public void verifyInvalidAccessType() throws Exception { @Test public void verifyUnsupportedAccessType() throws Exception { - final MockHttpServletRequest mockRequest = new MockHttpServletRequest("GET", CONTEXT - + OAuthConstants.AUTHORIZE_URL); + + final MockHttpServletRequest mockRequest + = new MockHttpServletRequest("GET", CONTEXT + OAuthConstants.AUTHORIZE_URL); mockRequest.setParameter(OAuthConstants.RESPONSE_TYPE, RESPONSE_TYPE); mockRequest.setParameter(OAuthConstants.CLIENT_ID, CLIENT_ID); mockRequest.setParameter(OAuthConstants.REDIRECT_URI, REDIRECT_URI); @@ -175,14 +184,15 @@ public void verifyUnsupportedAccessType() throws Exception { @Test public void verifyNoRegisteredService() throws Exception { + final OAuthRegisteredService service = getRegisteredService(REDIRECT_URI, CLIENT_ID); final CentralOAuthService centralOAuthService = mock(CentralOAuthService.class); when(centralOAuthService.getRegisteredService(CLIENT_ID)).thenReturn(service); when(centralOAuthService.getRegisteredService(NO_SUCH_CLIENT_ID)).thenReturn(null); - final MockHttpServletRequest mockRequest = new MockHttpServletRequest("GET", CONTEXT - + OAuthConstants.AUTHORIZE_URL); + final MockHttpServletRequest mockRequest + = new MockHttpServletRequest("GET", CONTEXT + OAuthConstants.AUTHORIZE_URL); mockRequest.setParameter(OAuthConstants.RESPONSE_TYPE, RESPONSE_TYPE); mockRequest.setParameter(OAuthConstants.CLIENT_ID, NO_SUCH_CLIENT_ID); mockRequest.setParameter(OAuthConstants.REDIRECT_URI, REDIRECT_URI); @@ -200,13 +210,14 @@ public void verifyNoRegisteredService() throws Exception { @Test public void verifyRedirectUriDoesNotStartWithServiceId() throws Exception { + final OAuthRegisteredService service = getRegisteredService(REDIRECT_URI, CLIENT_ID); final CentralOAuthService centralOAuthService = mock(CentralOAuthService.class); when(centralOAuthService.getRegisteredService(CLIENT_ID)).thenReturn(service); - final MockHttpServletRequest mockRequest = new MockHttpServletRequest("GET", CONTEXT - + OAuthConstants.AUTHORIZE_URL); + final MockHttpServletRequest mockRequest + = new MockHttpServletRequest("GET", CONTEXT + OAuthConstants.AUTHORIZE_URL); mockRequest.setParameter(OAuthConstants.RESPONSE_TYPE, RESPONSE_TYPE); mockRequest.setParameter(OAuthConstants.CLIENT_ID, CLIENT_ID); mockRequest.setParameter(OAuthConstants.REDIRECT_URI, OTHER_REDIRECT_URI); @@ -224,13 +235,14 @@ public void verifyRedirectUriDoesNotStartWithServiceId() throws Exception { @Test public void verifyOK() throws Exception { + final OAuthRegisteredService service = getRegisteredService(REDIRECT_URI, SERVICE_NAME); final CentralOAuthService centralOAuthService = mock(CentralOAuthService.class); when(centralOAuthService.getRegisteredService(CLIENT_ID)).thenReturn(service); - final MockHttpServletRequest mockRequest = new MockHttpServletRequest("GET", CONTEXT - + OAuthConstants.AUTHORIZE_URL); + final MockHttpServletRequest mockRequest + = new MockHttpServletRequest("GET", CONTEXT + OAuthConstants.AUTHORIZE_URL); mockRequest.setParameter(OAuthConstants.RESPONSE_TYPE, RESPONSE_TYPE); mockRequest.setParameter(OAuthConstants.CLIENT_ID, CLIENT_ID); mockRequest.setParameter(OAuthConstants.REDIRECT_URI, REDIRECT_URI); @@ -247,6 +259,7 @@ public void verifyOK() throws Exception { final ModelAndView modelAndView = oauth20WrapperController.handleRequest(mockRequest, mockResponse); assertTrue(modelAndView.getView() instanceof RedirectView); + final RedirectView redirectView = (RedirectView) modelAndView.getView(); assertTrue(redirectView.getUrl().contains("?service=http")); assertTrue(redirectView.getUrl().endsWith(OAuthConstants.CALLBACK_AUTHORIZE_URL)); @@ -263,18 +276,17 @@ public void verifyOK() throws Exception { assertEquals(STATE, session.getAttribute(OAuthConstants.OAUTH20_STATE)); } - /** - * Verify that attempts to use regex for redirect URI matching fails with expected exception. - */ + /** Verify that attempts to use regex for redirect URI matching fails with expected exception. */ @Test public void verifyRegexRedirectUriDoesNotStartWithServiceId() throws Exception { + final OAuthRegisteredService service = getRegisteredService(REGEX_REDIRECT_URI, CLIENT_ID); final CentralOAuthService centralOAuthService = mock(CentralOAuthService.class); when(centralOAuthService.getRegisteredService(CLIENT_ID)).thenReturn(service); - final MockHttpServletRequest mockRequest = new MockHttpServletRequest("GET", CONTEXT - + OAuthConstants.AUTHORIZE_URL); + final MockHttpServletRequest mockRequest + = new MockHttpServletRequest("GET", CONTEXT + OAuthConstants.AUTHORIZE_URL); mockRequest.setParameter(OAuthConstants.RESPONSE_TYPE, RESPONSE_TYPE); mockRequest.setParameter(OAuthConstants.CLIENT_ID, CLIENT_ID); mockRequest.setParameter(OAuthConstants.REDIRECT_URI, OTHER_REGEX_REDIRECT_URI); @@ -290,18 +302,17 @@ public void verifyRegexRedirectUriDoesNotStartWithServiceId() throws Exception { assertEquals(OAuthConstants.ERROR_VIEW, modelAndView.getViewName()); } - /** - * Verify that regex-like redirect URI matches if an only if the two string are equal (case-insensitive). - */ + /** Verify that regex-like redirect URI matches if an only if the two string are equal (case-insensitive). */ @Test public void verifyOKRegexRedirectUri() throws Exception { + final OAuthRegisteredService service = getRegisteredService(REGEX_REDIRECT_URI, SERVICE_NAME); final CentralOAuthService centralOAuthService = mock(CentralOAuthService.class); when(centralOAuthService.getRegisteredService(CLIENT_ID)).thenReturn(service); - final MockHttpServletRequest mockRequest = new MockHttpServletRequest("GET", CONTEXT - + OAuthConstants.AUTHORIZE_URL); + final MockHttpServletRequest mockRequest + = new MockHttpServletRequest("GET", CONTEXT + OAuthConstants.AUTHORIZE_URL); mockRequest.setParameter(OAuthConstants.RESPONSE_TYPE, RESPONSE_TYPE); mockRequest.setParameter(OAuthConstants.CLIENT_ID, CLIENT_ID); mockRequest.setParameter(OAuthConstants.REDIRECT_URI, REGEX_REDIRECT_URI); @@ -318,6 +329,7 @@ public void verifyOKRegexRedirectUri() throws Exception { final ModelAndView modelAndView = oauth20WrapperController.handleRequest(mockRequest, mockResponse); assertTrue(modelAndView.getView() instanceof RedirectView); + final RedirectView redirectView = (RedirectView) modelAndView.getView(); assertTrue(redirectView.getUrl().contains("?service=http")); assertTrue(redirectView.getUrl().endsWith(OAuthConstants.CALLBACK_AUTHORIZE_URL)); @@ -336,13 +348,14 @@ public void verifyOKRegexRedirectUri() throws Exception { @Test public void verifyDefaultsOK() throws Exception { + final OAuthRegisteredService service = getRegisteredService(REDIRECT_URI, SERVICE_NAME); final CentralOAuthService centralOAuthService = mock(CentralOAuthService.class); when(centralOAuthService.getRegisteredService(CLIENT_ID)).thenReturn(service); - final MockHttpServletRequest mockRequest = new MockHttpServletRequest("GET", CONTEXT - + OAuthConstants.AUTHORIZE_URL); + final MockHttpServletRequest mockRequest + = new MockHttpServletRequest("GET", CONTEXT + OAuthConstants.AUTHORIZE_URL); mockRequest.setParameter(OAuthConstants.CLIENT_ID, CLIENT_ID); mockRequest.setParameter(OAuthConstants.REDIRECT_URI, REDIRECT_URI); mockRequest.setParameter(OAuthConstants.STATE, STATE); @@ -356,6 +369,7 @@ public void verifyDefaultsOK() throws Exception { final ModelAndView modelAndView = oauth20WrapperController.handleRequest(mockRequest, mockResponse); assertTrue(modelAndView.getView() instanceof RedirectView); + final RedirectView redirectView = (RedirectView) modelAndView.getView(); assertTrue(redirectView.getUrl().contains("?service=http")); assertTrue(redirectView.getUrl().endsWith(OAuthConstants.CALLBACK_AUTHORIZE_URL)); @@ -373,6 +387,7 @@ public void verifyDefaultsOK() throws Exception { } private OAuthRegisteredService getRegisteredService(final String serviceId, final String name) { + final OAuthRegisteredService registeredServiceImpl = new OAuthRegisteredService(); registeredServiceImpl.setName(name); diff --git a/cas-server-support-oauth/src/test/java/org/jasig/cas/support/oauth/web/OAuth20MetadataClientControllerTests.java b/cas-server-support-oauth/src/test/java/org/jasig/cas/support/oauth/web/OAuth20MetadataClientControllerTests.java index bb2af736..8625967b 100644 --- a/cas-server-support-oauth/src/test/java/org/jasig/cas/support/oauth/web/OAuth20MetadataClientControllerTests.java +++ b/cas-server-support-oauth/src/test/java/org/jasig/cas/support/oauth/web/OAuth20MetadataClientControllerTests.java @@ -19,26 +19,31 @@ package org.jasig.cas.support.oauth.web; import org.apache.http.HttpStatus; + import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; + import org.jasig.cas.support.oauth.CentralOAuthService; import org.jasig.cas.support.oauth.OAuthConstants; import org.jasig.cas.support.oauth.metadata.ClientMetadata; -import org.junit.Test; -import org.springframework.mock.web.MockHttpServletRequest; -import org.springframework.mock.web.MockHttpServletResponse; -import org.springframework.web.servlet.ModelAndView; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; +import org.junit.Test; + import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.web.servlet.ModelAndView; + /** * This class tests the {@link OAuth20MetadataClientController} class. * * @author Fitz Elliott - * @since 3.5.2 + * @author Longze Chen + * @since 4.1.5 */ public final class OAuth20MetadataClientControllerTests { @@ -65,13 +70,15 @@ public final class OAuth20MetadataClientControllerTests { @Test public void verifyNoClientId() throws Exception { + final CentralOAuthService centralOAuthService = mock(CentralOAuthService.class); when(centralOAuthService.getClientMetadata(CLIENT_ID, CLIENT_SECRET)).thenReturn(METADATA); - final MockHttpServletRequest mockRequest = new MockHttpServletRequest("POST", CONTEXT - + OAuthConstants.METADATA_URL); + final MockHttpServletRequest mockRequest + = new MockHttpServletRequest("POST", CONTEXT + OAuthConstants.METADATA_URL); mockRequest.setParameter(OAuthConstants.CLIENT_ID, ""); mockRequest.setParameter(OAuthConstants.CLIENT_SECRET, CLIENT_SECRET); + final MockHttpServletResponse mockResponse = new MockHttpServletResponse(); final OAuth20WrapperController oauth20WrapperController = new OAuth20WrapperController(); @@ -83,8 +90,8 @@ public void verifyNoClientId() throws Exception { assertEquals(HttpStatus.SC_BAD_REQUEST, mockResponse.getStatus()); assertEquals(CONTENT_TYPE, mockResponse.getContentType()); - final String expected = "{\"error\":\"" + OAuthConstants.INVALID_REQUEST + "\",\"error_description\":\"" - + "Invalid or missing parameter 'client_id'\"}"; + final String expected = "{\"error\":\"" + OAuthConstants.INVALID_REQUEST + + "\",\"error_description\":\"" + "Invalid or missing parameter 'client_id'\"}"; final ObjectMapper mapper = new ObjectMapper(); final JsonNode expectedObj = mapper.readTree(expected); @@ -95,13 +102,15 @@ public void verifyNoClientId() throws Exception { @Test public void verifyNoClientSecret() throws Exception { + final CentralOAuthService centralOAuthService = mock(CentralOAuthService.class); when(centralOAuthService.getClientMetadata(CLIENT_ID, CLIENT_SECRET)).thenReturn(METADATA); - final MockHttpServletRequest mockRequest = new MockHttpServletRequest("POST", CONTEXT - + OAuthConstants.METADATA_URL); + final MockHttpServletRequest mockRequest + = new MockHttpServletRequest("POST", CONTEXT + OAuthConstants.METADATA_URL); mockRequest.setParameter(OAuthConstants.CLIENT_ID, CLIENT_ID); mockRequest.setParameter(OAuthConstants.CLIENT_SECRET, ""); + final MockHttpServletResponse mockResponse = new MockHttpServletResponse(); final OAuth20WrapperController oauth20WrapperController = new OAuth20WrapperController(); @@ -113,8 +122,8 @@ public void verifyNoClientSecret() throws Exception { assertEquals(HttpStatus.SC_BAD_REQUEST, mockResponse.getStatus()); assertEquals(CONTENT_TYPE, mockResponse.getContentType()); - final String expected = "{\"error\":\"" + OAuthConstants.INVALID_REQUEST + "\",\"error_description\":\"" - + "Invalid or missing parameter 'client_secret'\"}"; + final String expected = "{\"error\":\"" + OAuthConstants.INVALID_REQUEST + + "\",\"error_description\":\"" + "Invalid or missing parameter 'client_secret'\"}"; final ObjectMapper mapper = new ObjectMapper(); final JsonNode expectedObj = mapper.readTree(expected); @@ -125,15 +134,17 @@ public void verifyNoClientSecret() throws Exception { @Test public void verifyOK() throws Exception { + final CentralOAuthService centralOAuthService = mock(CentralOAuthService.class); when(centralOAuthService.getClientMetadata(NO_SUCH_CLIENT_ID, CLIENT_SECRET)).thenReturn(null); when(centralOAuthService.getClientMetadata(CLIENT_ID, WRONG_CLIENT_SECRET)).thenReturn(null); when(centralOAuthService.getClientMetadata(CLIENT_ID, CLIENT_SECRET)).thenReturn(METADATA); - final MockHttpServletRequest mockRequest = new MockHttpServletRequest("POST", CONTEXT - + OAuthConstants.METADATA_URL); + final MockHttpServletRequest mockRequest + = new MockHttpServletRequest("POST", CONTEXT + OAuthConstants.METADATA_URL); mockRequest.setParameter(OAuthConstants.CLIENT_ID, CLIENT_ID); mockRequest.setParameter(OAuthConstants.CLIENT_SECRET, CLIENT_SECRET); + final MockHttpServletResponse mockResponse = new MockHttpServletResponse(); final OAuth20WrapperController oauth20WrapperController = new OAuth20WrapperController(); @@ -145,9 +156,8 @@ public void verifyOK() throws Exception { assertEquals(HttpStatus.SC_OK, mockResponse.getStatus()); assertEquals(CONTENT_TYPE, mockResponse.getContentType()); - final String expected = "{\"client_id\":\"" + CLIENT_ID + "\",\"name\":\"" - + CLIENT_META_NAME + "\",\"description\":\"" + CLIENT_META_DESCRIPTION - + "\",\"users\":\"" + CLIENT_META_USERS + "\"}"; + final String expected = "{\"client_id\":\"" + CLIENT_ID + "\",\"name\":\"" + CLIENT_META_NAME + + "\",\"description\":\"" + CLIENT_META_DESCRIPTION + "\",\"users\":\"" + CLIENT_META_USERS + "\"}"; final ObjectMapper mapper = new ObjectMapper(); final JsonNode expectedObj = mapper.readTree(expected); @@ -160,15 +170,17 @@ public void verifyOK() throws Exception { @Test public void verifyNoSuchClientId() throws Exception { + final CentralOAuthService centralOAuthService = mock(CentralOAuthService.class); when(centralOAuthService.getClientMetadata(NO_SUCH_CLIENT_ID, CLIENT_SECRET)).thenReturn(null); when(centralOAuthService.getClientMetadata(CLIENT_ID, WRONG_CLIENT_SECRET)).thenReturn(null); when(centralOAuthService.getClientMetadata(CLIENT_ID, CLIENT_SECRET)).thenReturn(METADATA); - final MockHttpServletRequest mockRequest = new MockHttpServletRequest("POST", CONTEXT - + OAuthConstants.METADATA_URL); + final MockHttpServletRequest mockRequest + = new MockHttpServletRequest("POST", CONTEXT + OAuthConstants.METADATA_URL); mockRequest.setParameter(OAuthConstants.CLIENT_ID, NO_SUCH_CLIENT_ID); mockRequest.setParameter(OAuthConstants.CLIENT_SECRET, CLIENT_SECRET); + final MockHttpServletResponse mockResponse = new MockHttpServletResponse(); final OAuth20WrapperController oauth20WrapperController = new OAuth20WrapperController(); @@ -180,8 +192,8 @@ public void verifyNoSuchClientId() throws Exception { assertEquals(HttpStatus.SC_BAD_REQUEST, mockResponse.getStatus()); assertEquals(CONTENT_TYPE, mockResponse.getContentType()); - final String expected = "{\"error\":\"" + OAuthConstants.INVALID_REQUEST + "\",\"error_description\":\"" - + OAuthConstants.INVALID_CLIENT_ID_OR_SECRET_DESCRIPTION + "\"}"; + final String expected = "{\"error\":\"" + OAuthConstants.INVALID_REQUEST + + "\",\"error_description\":\"" + OAuthConstants.INVALID_CLIENT_ID_OR_SECRET_DESCRIPTION + "\"}"; final ObjectMapper mapper = new ObjectMapper(); final JsonNode expectedObj = mapper.readTree(expected); @@ -192,13 +204,14 @@ public void verifyNoSuchClientId() throws Exception { @Test public void verifyWrongClientSecret() throws Exception { + final CentralOAuthService centralOAuthService = mock(CentralOAuthService.class); when(centralOAuthService.getClientMetadata(NO_SUCH_CLIENT_ID, CLIENT_SECRET)).thenReturn(null); when(centralOAuthService.getClientMetadata(CLIENT_ID, WRONG_CLIENT_SECRET)).thenReturn(null); when(centralOAuthService.getClientMetadata(CLIENT_ID, CLIENT_SECRET)).thenReturn(METADATA); - final MockHttpServletRequest mockRequest = new MockHttpServletRequest("POST", CONTEXT - + OAuthConstants.METADATA_URL); + final MockHttpServletRequest mockRequest + = new MockHttpServletRequest("POST", CONTEXT + OAuthConstants.METADATA_URL); mockRequest.setParameter(OAuthConstants.CLIENT_ID, CLIENT_ID); mockRequest.setParameter(OAuthConstants.CLIENT_SECRET, WRONG_CLIENT_SECRET); final MockHttpServletResponse mockResponse = new MockHttpServletResponse(); @@ -212,8 +225,8 @@ public void verifyWrongClientSecret() throws Exception { assertEquals(HttpStatus.SC_BAD_REQUEST, mockResponse.getStatus()); assertEquals(CONTENT_TYPE, mockResponse.getContentType()); - final String expected = "{\"error\":\"" + OAuthConstants.INVALID_REQUEST + "\",\"error_description\":\"" - + OAuthConstants.INVALID_CLIENT_ID_OR_SECRET_DESCRIPTION + "\"}"; + final String expected = "{\"error\":\"" + OAuthConstants.INVALID_REQUEST + + "\",\"error_description\":\"" + OAuthConstants.INVALID_CLIENT_ID_OR_SECRET_DESCRIPTION + "\"}"; final ObjectMapper mapper = new ObjectMapper(); final JsonNode expectedObj = mapper.readTree(expected); diff --git a/cas-server-support-oauth/src/test/java/org/jasig/cas/support/oauth/web/OAuth20MetadataPrincipalControllerTests.java b/cas-server-support-oauth/src/test/java/org/jasig/cas/support/oauth/web/OAuth20MetadataPrincipalControllerTests.java index ec941959..5bab346c 100644 --- a/cas-server-support-oauth/src/test/java/org/jasig/cas/support/oauth/web/OAuth20MetadataPrincipalControllerTests.java +++ b/cas-server-support-oauth/src/test/java/org/jasig/cas/support/oauth/web/OAuth20MetadataPrincipalControllerTests.java @@ -19,32 +19,37 @@ package org.jasig.cas.support.oauth.web; import org.apache.http.HttpStatus; + import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; + import org.jasig.cas.support.oauth.CentralOAuthService; import org.jasig.cas.support.oauth.OAuthConstants; import org.jasig.cas.support.oauth.metadata.PrincipalMetadata; import org.jasig.cas.support.oauth.token.AccessToken; import org.jasig.cas.support.oauth.token.InvalidTokenException; -import org.junit.Test; -import org.springframework.mock.web.MockHttpServletRequest; -import org.springframework.mock.web.MockHttpServletResponse; -import org.springframework.web.servlet.ModelAndView; - import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; +import org.junit.Test; + import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.web.servlet.ModelAndView; + import java.util.Arrays; +import java.util.Collections; import java.util.List; /** * This class tests the {@link OAuth20MetadataPrincipalController} class. * * @author Fitz Elliott - * @since 3.5.2 + * @author Longze Chen + * @since 4.1.5 */ public final class OAuth20MetadataPrincipalControllerTests { @@ -66,8 +71,10 @@ public final class OAuth20MetadataPrincipalControllerTests { @Test public void verifyNoTokenOrAuthHeader() throws Exception { - final MockHttpServletRequest mockRequest = new MockHttpServletRequest("POST", CONTEXT - + OAuthConstants.METADATA_URL); + + final MockHttpServletRequest mockRequest + = new MockHttpServletRequest("POST", CONTEXT + OAuthConstants.METADATA_URL); + final MockHttpServletResponse mockResponse = new MockHttpServletResponse(); final OAuth20WrapperController oauth20WrapperController = new OAuth20WrapperController(); @@ -78,8 +85,8 @@ public void verifyNoTokenOrAuthHeader() throws Exception { assertEquals(HttpStatus.SC_BAD_REQUEST, mockResponse.getStatus()); assertEquals(CONTENT_TYPE, mockResponse.getContentType()); - final String expected = "{\"error\":\"" + OAuthConstants.MISSING_ACCESS_TOKEN + "\",\"error_description\":\"" - + OAuthConstants.MISSING_ACCESS_TOKEN_DESCRIPTION + "\"}"; + final String expected = "{\"error\":\"" + OAuthConstants.INVALID_REQUEST + + "\",\"error_description\":\"" + "Invalid or missing parameter 'access_token'\"}"; final ObjectMapper mapper = new ObjectMapper(); final JsonNode expectedObj = mapper.readTree(expected); @@ -91,9 +98,11 @@ public void verifyNoTokenOrAuthHeader() throws Exception { @Test public void verifyNoTokenAndAuthHeaderIsBlank() throws Exception { - final MockHttpServletRequest mockRequest = new MockHttpServletRequest("POST", CONTEXT - + OAuthConstants.METADATA_URL); + + final MockHttpServletRequest mockRequest + = new MockHttpServletRequest("POST", CONTEXT + OAuthConstants.METADATA_URL); mockRequest.addHeader("Authorization", ""); + final MockHttpServletResponse mockResponse = new MockHttpServletResponse(); final OAuth20WrapperController oauth20WrapperController = new OAuth20WrapperController(); @@ -104,8 +113,8 @@ public void verifyNoTokenAndAuthHeaderIsBlank() throws Exception { assertEquals(HttpStatus.SC_BAD_REQUEST, mockResponse.getStatus()); assertEquals(CONTENT_TYPE, mockResponse.getContentType()); - final String expected = "{\"error\":\"" + OAuthConstants.MISSING_ACCESS_TOKEN + "\",\"error_description\":\"" - + OAuthConstants.MISSING_ACCESS_TOKEN_DESCRIPTION + "\"}"; + final String expected = "{\"error\":\"" + OAuthConstants.INVALID_REQUEST + + "\",\"error_description\":\"" + "Invalid or missing parameter 'access_token'\"}"; final ObjectMapper mapper = new ObjectMapper(); final JsonNode expectedObj = mapper.readTree(expected); @@ -116,9 +125,11 @@ public void verifyNoTokenAndAuthHeaderIsBlank() throws Exception { @Test public void verifyNoTokenAndAuthHeaderIsMalformed() throws Exception { - final MockHttpServletRequest mockRequest = new MockHttpServletRequest("POST", CONTEXT - + OAuthConstants.METADATA_URL); + + final MockHttpServletRequest mockRequest + = new MockHttpServletRequest("POST", CONTEXT + OAuthConstants.METADATA_URL); mockRequest.addHeader("Authorization", "Let me in i am authorized"); + final MockHttpServletResponse mockResponse = new MockHttpServletResponse(); final OAuth20WrapperController oauth20WrapperController = new OAuth20WrapperController(); @@ -129,8 +140,8 @@ public void verifyNoTokenAndAuthHeaderIsMalformed() throws Exception { assertEquals(HttpStatus.SC_BAD_REQUEST, mockResponse.getStatus()); assertEquals(CONTENT_TYPE, mockResponse.getContentType()); - final String expected = "{\"error\":\"" + OAuthConstants.MISSING_ACCESS_TOKEN + "\",\"error_description\":\"" - + OAuthConstants.MISSING_ACCESS_TOKEN_DESCRIPTION + "\"}"; + final String expected = "{\"error\":\"" + OAuthConstants.INVALID_REQUEST + + "\",\"error_description\":\"" + "Invalid or missing parameter 'access_token'\"}"; final ObjectMapper mapper = new ObjectMapper(); final JsonNode expectedObj = mapper.readTree(expected); @@ -141,13 +152,15 @@ public void verifyNoTokenAndAuthHeaderIsMalformed() throws Exception { @Test public void verifyInvalidAccessToken() throws Exception { + final CentralOAuthService centralOAuthService = mock(CentralOAuthService.class); when(centralOAuthService.getToken(AT_ID, AccessToken.class)).thenThrow(new InvalidTokenException("error")); when(centralOAuthService.getPersonalAccessToken(AT_ID)).thenReturn(null); - final MockHttpServletRequest mockRequest = new MockHttpServletRequest("GET", CONTEXT - + OAuthConstants.PROFILE_URL); + final MockHttpServletRequest mockRequest + = new MockHttpServletRequest("GET", CONTEXT + OAuthConstants.PROFILE_URL); mockRequest.setParameter(OAuthConstants.ACCESS_TOKEN, AT_ID); + final MockHttpServletResponse mockResponse = new MockHttpServletResponse(); final OAuth20WrapperController oauth20WrapperController = new OAuth20WrapperController(); @@ -161,8 +174,8 @@ public void verifyInvalidAccessToken() throws Exception { final ObjectMapper mapper = new ObjectMapper(); - final String expected = "{\"error\":\"" + OAuthConstants.UNAUTHORIZED_REQUEST + "\",\"error_description\":\"" - + OAuthConstants.INVALID_ACCESS_TOKEN_DESCRIPTION + "\"}"; + final String expected = "{\"error\":\"" + OAuthConstants.UNAUTHORIZED_REQUEST + + "\",\"error_description\":\"" + OAuthConstants.INVALID_ACCESS_TOKEN_DESCRIPTION + "\"}"; final JsonNode expectedObj = mapper.readTree(expected); final JsonNode receivedObj = mapper.readTree(mockResponse.getContentAsString()); assertEquals(expectedObj.get("error").asText(), receivedObj.get("error").asText()); @@ -171,20 +184,23 @@ public void verifyInvalidAccessToken() throws Exception { @Test public void verifyOKWithAccessToken() throws Exception { + final AccessToken accessToken = mock(AccessToken.class); when(accessToken.getId()).thenReturn(AT_ID); final List principalMetas = Arrays.asList( new PrincipalMetadata(CLIENT_ID, PRINC_NAME_ONE, PRINC_DESCR_ONE), - new PrincipalMetadata(CLIENT_ID, PRINC_NAME_TWO, PRINC_DESCR_TWO)); + new PrincipalMetadata(CLIENT_ID, PRINC_NAME_TWO, PRINC_DESCR_TWO) + ); final CentralOAuthService centralOAuthService = mock(CentralOAuthService.class); when(centralOAuthService.getToken(AT_ID, AccessToken.class)).thenReturn(accessToken); when(centralOAuthService.getPrincipalMetadata(accessToken)).thenReturn(principalMetas); - final MockHttpServletRequest mockRequest = new MockHttpServletRequest("POST", CONTEXT - + OAuthConstants.METADATA_URL); + final MockHttpServletRequest mockRequest + = new MockHttpServletRequest("POST", CONTEXT + OAuthConstants.METADATA_URL); mockRequest.setParameter(OAuthConstants.ACCESS_TOKEN, AT_ID); + final MockHttpServletResponse mockResponse = new MockHttpServletResponse(); final OAuth20WrapperController oauth20WrapperController = new OAuth20WrapperController(); @@ -227,20 +243,23 @@ public void verifyOKWithAccessToken() throws Exception { @Test public void verifyOKWithAuthHeader() throws Exception { + final AccessToken accessToken = mock(AccessToken.class); when(accessToken.getId()).thenReturn(AT_ID); final List principalMetas = Arrays.asList( new PrincipalMetadata(CLIENT_ID, PRINC_NAME_ONE, PRINC_DESCR_ONE), - new PrincipalMetadata(CLIENT_ID, PRINC_NAME_TWO, PRINC_DESCR_TWO)); + new PrincipalMetadata(CLIENT_ID, PRINC_NAME_TWO, PRINC_DESCR_TWO) + ); final CentralOAuthService centralOAuthService = mock(CentralOAuthService.class); when(centralOAuthService.getToken(AT_ID, AccessToken.class)).thenReturn(accessToken); when(centralOAuthService.getPrincipalMetadata(accessToken)).thenReturn(principalMetas); - final MockHttpServletRequest mockRequest = new MockHttpServletRequest("POST", CONTEXT - + OAuthConstants.METADATA_URL); + final MockHttpServletRequest mockRequest + = new MockHttpServletRequest("POST", CONTEXT + OAuthConstants.METADATA_URL); mockRequest.addHeader("Authorization", OAuthConstants.BEARER_TOKEN + " " + AT_ID); + final MockHttpServletResponse mockResponse = new MockHttpServletResponse(); final OAuth20WrapperController oauth20WrapperController = new OAuth20WrapperController(); @@ -283,18 +302,20 @@ public void verifyOKWithAuthHeader() throws Exception { @Test public void verifyEmptyPrincipalListOK() throws Exception { + final AccessToken accessToken = mock(AccessToken.class); when(accessToken.getId()).thenReturn(AT_ID); - final List principalMetas = Arrays.asList(); + final List principalMetas = Collections.emptyList(); final CentralOAuthService centralOAuthService = mock(CentralOAuthService.class); when(centralOAuthService.getToken(AT_ID, AccessToken.class)).thenReturn(accessToken); when(centralOAuthService.getPrincipalMetadata(accessToken)).thenReturn(principalMetas); - final MockHttpServletRequest mockRequest = new MockHttpServletRequest("POST", CONTEXT - + OAuthConstants.METADATA_URL); + final MockHttpServletRequest mockRequest + = new MockHttpServletRequest("POST", CONTEXT + OAuthConstants.METADATA_URL); mockRequest.setParameter(OAuthConstants.ACCESS_TOKEN, AT_ID); + final MockHttpServletResponse mockResponse = new MockHttpServletResponse(); final OAuth20WrapperController oauth20WrapperController = new OAuth20WrapperController(); diff --git a/cas-server-support-oauth/src/test/java/org/jasig/cas/support/oauth/web/OAuth20ProfileControllerTests.java b/cas-server-support-oauth/src/test/java/org/jasig/cas/support/oauth/web/OAuth20ProfileControllerTests.java index 1eec4cb3..10f26fb8 100644 --- a/cas-server-support-oauth/src/test/java/org/jasig/cas/support/oauth/web/OAuth20ProfileControllerTests.java +++ b/cas-server-support-oauth/src/test/java/org/jasig/cas/support/oauth/web/OAuth20ProfileControllerTests.java @@ -20,7 +20,9 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; + import org.apache.http.HttpStatus; + import org.jasig.cas.CentralAuthenticationService; import org.jasig.cas.authentication.Authentication; import org.jasig.cas.authentication.principal.Principal; @@ -35,7 +37,14 @@ import org.jasig.cas.ticket.ServiceTicket; import org.jasig.cas.ticket.TicketGrantingTicket; import org.jasig.cas.validation.Assertion; + import org.junit.Test; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.web.servlet.ModelAndView; @@ -47,17 +56,13 @@ import java.util.Map; import java.util.Set; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNull; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - /** * This class tests the {@link OAuth20ProfileController} class. * * @author Jerome Leleu * @author Michael Haselton - * @since 3.5.2 + * @author Longze Chen + * @since 4.1.5 */ public final class OAuth20ProfileControllerTests { @@ -77,8 +82,10 @@ public final class OAuth20ProfileControllerTests { @Test public void verifyNoAccessToken() throws Exception { - final MockHttpServletRequest mockRequest = new MockHttpServletRequest("GET", CONTEXT - + OAuthConstants.PROFILE_URL); + + final MockHttpServletRequest mockRequest + = new MockHttpServletRequest("GET", CONTEXT + OAuthConstants.PROFILE_URL); + final MockHttpServletResponse mockResponse = new MockHttpServletResponse(); final OAuth20WrapperController oauth20WrapperController = new OAuth20WrapperController(); @@ -90,9 +97,8 @@ public void verifyNoAccessToken() throws Exception { assertEquals(CONTENT_TYPE, mockResponse.getContentType()); final ObjectMapper mapper = new ObjectMapper(); - - final String expected = "{\"error\":\"" + OAuthConstants.MISSING_ACCESS_TOKEN + "\",\"error_description\":\"" - + OAuthConstants.MISSING_ACCESS_TOKEN_DESCRIPTION + "\"}"; + final String expected = "{\"error\":\"" + OAuthConstants.INVALID_REQUEST + + "\",\"error_description\":\"" + "Invalid or missing parameter 'access_token'\"}"; final JsonNode expectedObj = mapper.readTree(expected); final JsonNode receivedObj = mapper.readTree(mockResponse.getContentAsString()); assertEquals(expectedObj.get("error").asText(), receivedObj.get("error").asText()); @@ -101,9 +107,11 @@ public void verifyNoAccessToken() throws Exception { @Test public void verifyNoTokenAndAuthHeaderIsMalformed() throws Exception { - final MockHttpServletRequest mockRequest = new MockHttpServletRequest("GET", CONTEXT - + OAuthConstants.PROFILE_URL); + + final MockHttpServletRequest mockRequest + = new MockHttpServletRequest("GET", CONTEXT + OAuthConstants.PROFILE_URL); mockRequest.addHeader("Authorization", "Let me in i am authorized"); + final MockHttpServletResponse mockResponse = new MockHttpServletResponse(); final OAuth20WrapperController oauth20WrapperController = new OAuth20WrapperController(); @@ -114,8 +122,8 @@ public void verifyNoTokenAndAuthHeaderIsMalformed() throws Exception { assertEquals(HttpStatus.SC_BAD_REQUEST, mockResponse.getStatus()); assertEquals(CONTENT_TYPE, mockResponse.getContentType()); - final String expected = "{\"error\":\"" + OAuthConstants.MISSING_ACCESS_TOKEN + "\",\"error_description\":\"" - + OAuthConstants.MISSING_ACCESS_TOKEN_DESCRIPTION + "\"}"; + final String expected = "{\"error\":\"" + OAuthConstants.INVALID_REQUEST + + "\",\"error_description\":\"" + "Invalid or missing parameter 'access_token'\"}"; final ObjectMapper mapper = new ObjectMapper(); final JsonNode expectedObj = mapper.readTree(expected); @@ -126,13 +134,15 @@ public void verifyNoTokenAndAuthHeaderIsMalformed() throws Exception { @Test public void verifyInvalidAccessToken() throws Exception { + final CentralOAuthService centralOAuthService = mock(CentralOAuthService.class); when(centralOAuthService.getToken(AT_ID, AccessToken.class)).thenThrow(new InvalidTokenException("error")); when(centralOAuthService.getPersonalAccessToken(AT_ID)).thenReturn(null); - final MockHttpServletRequest mockRequest = new MockHttpServletRequest("GET", CONTEXT - + OAuthConstants.PROFILE_URL); + final MockHttpServletRequest mockRequest + = new MockHttpServletRequest("GET", CONTEXT + OAuthConstants.PROFILE_URL); mockRequest.setParameter(OAuthConstants.ACCESS_TOKEN, AT_ID); + final MockHttpServletResponse mockResponse = new MockHttpServletResponse(); final OAuth20WrapperController oauth20WrapperController = new OAuth20WrapperController(); @@ -145,9 +155,8 @@ public void verifyInvalidAccessToken() throws Exception { assertEquals(CONTENT_TYPE, mockResponse.getContentType()); final ObjectMapper mapper = new ObjectMapper(); - - final String expected = "{\"error\":\"" + OAuthConstants.UNAUTHORIZED_REQUEST + "\",\"error_description\":\"" - + OAuthConstants.INVALID_ACCESS_TOKEN_DESCRIPTION + "\"}"; + final String expected = "{\"error\":\"" + OAuthConstants.UNAUTHORIZED_REQUEST + + "\",\"error_description\":\"" + OAuthConstants.INVALID_ACCESS_TOKEN_DESCRIPTION + "\"}"; final JsonNode expectedObj = mapper.readTree(expected); final JsonNode receivedObj = mapper.readTree(mockResponse.getContentAsString()); assertEquals(expectedObj.get("error").asText(), receivedObj.get("error").asText()); @@ -156,6 +165,7 @@ public void verifyInvalidAccessToken() throws Exception { @Test public void verifyInvalidValidateServiceTicket() throws Exception { + final TicketGrantingTicket ticketGrantingTicket = mock(TicketGrantingTicket.class); when(ticketGrantingTicket.isExpired()).thenReturn(false); @@ -175,13 +185,17 @@ public void verifyInvalidValidateServiceTicket() throws Exception { when(serviceTicket.getService()).thenReturn(service); final CentralAuthenticationService centralAuthenticationService = mock(CentralAuthenticationService.class); - when(centralAuthenticationService.grantServiceTicket(accessToken.getTicketGrantingTicket().getId(), - accessToken.getService())).thenReturn(serviceTicket); - when(centralAuthenticationService.validateServiceTicket(serviceTicket.getId(), - serviceTicket.getService())).thenThrow(new InvalidTicketException("expired ticket")); - - final MockHttpServletRequest mockRequest = new MockHttpServletRequest("GET", CONTEXT - + OAuthConstants.PROFILE_URL); + when(centralAuthenticationService.grantServiceTicket( + accessToken.getTicketGrantingTicket().getId(), + accessToken.getService() + )).thenReturn(serviceTicket); + when(centralAuthenticationService.validateServiceTicket( + serviceTicket.getId(), + serviceTicket.getService() + )).thenThrow(new InvalidTicketException("expired ticket")); + + final MockHttpServletRequest mockRequest + = new MockHttpServletRequest("GET", CONTEXT + OAuthConstants.PROFILE_URL); mockRequest.setParameter(OAuthConstants.ACCESS_TOKEN, AT_ID); final MockHttpServletResponse mockResponse = new MockHttpServletResponse(); @@ -198,8 +212,8 @@ public void verifyInvalidValidateServiceTicket() throws Exception { final ObjectMapper mapper = new ObjectMapper(); - final String expected = "{\"error\":\"" + OAuthConstants.UNAUTHORIZED_REQUEST + "\",\"error_description\":\"" - + OAuthConstants.INVALID_ACCESS_TOKEN_DESCRIPTION + "\"}"; + final String expected = "{\"error\":\"" + OAuthConstants.UNAUTHORIZED_REQUEST + + "\",\"error_description\":\"" + OAuthConstants.INVALID_ACCESS_TOKEN_DESCRIPTION + "\"}"; final JsonNode expectedObj = mapper.readTree(expected); final JsonNode receivedObj = mapper.readTree(mockResponse.getContentAsString()); assertEquals(expectedObj.get("error").asText(), receivedObj.get("error").asText()); @@ -208,6 +222,7 @@ public void verifyInvalidValidateServiceTicket() throws Exception { @Test public void verifyOK() throws Exception { + final TicketGrantingTicket ticketGrantingTicket = mock(TicketGrantingTicket.class); when(ticketGrantingTicket.isExpired()).thenReturn(false); @@ -242,13 +257,17 @@ public void verifyOK() throws Exception { when(assertion.getPrimaryAuthentication()).thenReturn(authentication); final CentralAuthenticationService centralAuthenticationService = mock(CentralAuthenticationService.class); - when(centralAuthenticationService.grantServiceTicket(accessToken.getTicketGrantingTicket().getId(), - accessToken.getService())).thenReturn(serviceTicket); - when(centralAuthenticationService.validateServiceTicket(serviceTicket.getId(), - serviceTicket.getService())).thenReturn(assertion); - - final MockHttpServletRequest mockRequest = new MockHttpServletRequest("GET", CONTEXT - + OAuthConstants.PROFILE_URL); + when(centralAuthenticationService.grantServiceTicket( + accessToken.getTicketGrantingTicket().getId(), + accessToken.getService() + )).thenReturn(serviceTicket); + when(centralAuthenticationService.validateServiceTicket( + serviceTicket.getId(), + serviceTicket.getService() + )).thenReturn(assertion); + + final MockHttpServletRequest mockRequest + = new MockHttpServletRequest("GET", CONTEXT + OAuthConstants.PROFILE_URL); mockRequest.setParameter(OAuthConstants.ACCESS_TOKEN, AT_ID); final MockHttpServletResponse mockResponse = new MockHttpServletResponse(); @@ -265,8 +284,8 @@ public void verifyOK() throws Exception { final ObjectMapper mapper = new ObjectMapper(); - final String expected = "{\"id\":\"" + ID + "\",\"attributes\":[{\"" + NAME + "\":\"" + VALUE + "\"},{\"" + NAME2 - + "\":[\"" + VALUE + "\",\"" + VALUE + "\"]}]}"; + final String expected = "{\"id\":\"" + ID + "\",\"attributes\":[{\"" + NAME + + "\":\"" + VALUE + "\"},{\"" + NAME2 + "\":[\"" + VALUE + "\",\"" + VALUE + "\"]}]}"; final JsonNode expectedObj = mapper.readTree(expected); final JsonNode receivedObj = mapper.readTree(mockResponse.getContentAsString()); assertEquals(expectedObj.get("id").asText(), receivedObj.get("id").asText()); @@ -280,6 +299,7 @@ public void verifyOK() throws Exception { @Test public void verifyOKWithAuthorizationHeader() throws Exception { + final TicketGrantingTicket ticketGrantingTicket = mock(TicketGrantingTicket.class); when(ticketGrantingTicket.isExpired()).thenReturn(false); @@ -314,16 +334,21 @@ public void verifyOKWithAuthorizationHeader() throws Exception { when(assertion.getPrimaryAuthentication()).thenReturn(authentication); final CentralAuthenticationService centralAuthenticationService = mock(CentralAuthenticationService.class); - when(centralAuthenticationService.grantServiceTicket(accessToken.getTicketGrantingTicket().getId(), - accessToken.getService())).thenReturn(serviceTicket); - when(centralAuthenticationService.validateServiceTicket(serviceTicket.getId(), - serviceTicket.getService())).thenReturn(assertion); - - final MockHttpServletRequest mockRequest = new MockHttpServletRequest("GET", CONTEXT - + OAuthConstants.PROFILE_URL); + when(centralAuthenticationService.grantServiceTicket( + accessToken.getTicketGrantingTicket().getId(), + accessToken.getService() + )).thenReturn(serviceTicket); + when(centralAuthenticationService.validateServiceTicket( + serviceTicket.getId(), + serviceTicket.getService() + )).thenReturn(assertion); + + final MockHttpServletRequest mockRequest + = new MockHttpServletRequest("GET", CONTEXT + OAuthConstants.PROFILE_URL); mockRequest.addHeader("Authorization", OAuthConstants.BEARER_TOKEN + " " + AT_ID); final MockHttpServletResponse mockResponse = new MockHttpServletResponse(); + final OAuth20WrapperController oauth20WrapperController = new OAuth20WrapperController(); oauth20WrapperController.setCentralOAuthService(centralOAuthService); oauth20WrapperController.setCentralAuthenticationService(centralAuthenticationService); @@ -336,8 +361,8 @@ public void verifyOKWithAuthorizationHeader() throws Exception { final ObjectMapper mapper = new ObjectMapper(); - final String expected = "{\"id\":\"" + ID + "\",\"attributes\":[{\"" + NAME + "\":\"" + VALUE + "\"},{\"" + NAME2 - + "\":[\"" + VALUE + "\",\"" + VALUE + "\"]}]}"; + final String expected = "{\"id\":\"" + ID + "\",\"attributes\":[{\"" + NAME + + "\":\"" + VALUE + "\"},{\"" + NAME2 + "\":[\"" + VALUE + "\",\"" + VALUE + "\"]}]}"; final JsonNode expectedObj = mapper.readTree(expected); final JsonNode receivedObj = mapper.readTree(mockResponse.getContentAsString()); assertEquals(expectedObj.get("id").asText(), receivedObj.get("id").asText()); @@ -351,6 +376,7 @@ public void verifyOKWithAuthorizationHeader() throws Exception { @Test public void verifyOKWithScopes() throws Exception { + final TicketGrantingTicket ticketGrantingTicket = mock(TicketGrantingTicket.class); when(ticketGrantingTicket.isExpired()).thenReturn(false); @@ -376,7 +402,7 @@ public void verifyOKWithScopes() throws Exception { final Principal principal = mock(Principal.class); when(principal.getId()).thenReturn(ID); - when(principal.getAttributes()).thenReturn(new HashMap()); + when(principal.getAttributes()).thenReturn(new HashMap<>()); final Authentication authentication = mock(Authentication.class); when(authentication.getPrincipal()).thenReturn(principal); @@ -385,16 +411,21 @@ public void verifyOKWithScopes() throws Exception { when(assertion.getPrimaryAuthentication()).thenReturn(authentication); final CentralAuthenticationService centralAuthenticationService = mock(CentralAuthenticationService.class); - when(centralAuthenticationService.grantServiceTicket(accessToken.getTicketGrantingTicket().getId(), - accessToken.getService())).thenReturn(serviceTicket); - when(centralAuthenticationService.validateServiceTicket(serviceTicket.getId(), - serviceTicket.getService())).thenReturn(assertion); - - final MockHttpServletRequest mockRequest = new MockHttpServletRequest("GET", CONTEXT - + OAuthConstants.PROFILE_URL); + when(centralAuthenticationService.grantServiceTicket( + accessToken.getTicketGrantingTicket().getId(), + accessToken.getService() + )).thenReturn(serviceTicket); + when(centralAuthenticationService.validateServiceTicket( + serviceTicket.getId(), + serviceTicket.getService() + )).thenReturn(assertion); + + final MockHttpServletRequest mockRequest + = new MockHttpServletRequest("GET", CONTEXT + OAuthConstants.PROFILE_URL); mockRequest.setParameter(OAuthConstants.ACCESS_TOKEN, AT_ID); final MockHttpServletResponse mockResponse = new MockHttpServletResponse(); + final OAuth20WrapperController oauth20WrapperController = new OAuth20WrapperController(); oauth20WrapperController.setCentralOAuthService(centralOAuthService); oauth20WrapperController.setCentralAuthenticationService(centralAuthenticationService); @@ -410,25 +441,23 @@ public void verifyOKWithScopes() throws Exception { final JsonNode expectedObj = mapper.readTree(expected); final JsonNode receivedObj = mapper.readTree(mockResponse.getContentAsString()); assertEquals(expectedObj.get("id").asText(), receivedObj.get("id").asText()); - assertEquals(expectedObj.get("scope").size(), receivedObj.get("scope").size()); for (final JsonNode expectedNode : expectedObj.get("scope")) { Boolean found = Boolean.FALSE; - for (final JsonNode receivedNode : receivedObj.get("scope")) { if (receivedNode.asText().equals(expectedNode.asText())) { found = Boolean.TRUE; break; } } - assertEquals(found, Boolean.TRUE); } } @Test public void verifyOKWithPersonalToken() throws Exception { + final Map map = new HashMap<>(); map.put(NAME, VALUE); final List list = Arrays.asList(VALUE, VALUE); @@ -456,8 +485,8 @@ public void verifyOKWithPersonalToken() throws Exception { final CentralOAuthService centralOAuthService = mock(CentralOAuthService.class); when(centralOAuthService.getToken(AT_ID, AccessToken.class)).thenReturn(accessToken); - final MockHttpServletRequest mockRequest = new MockHttpServletRequest("GET", CONTEXT - + OAuthConstants.PROFILE_URL); + final MockHttpServletRequest mockRequest + = new MockHttpServletRequest("GET", CONTEXT + OAuthConstants.PROFILE_URL); mockRequest.setParameter(OAuthConstants.ACCESS_TOKEN, AT_ID); final MockHttpServletResponse mockResponse = new MockHttpServletResponse(); @@ -481,6 +510,7 @@ public void verifyOKWithPersonalToken() throws Exception { @Test public void verifyOKWithOfflineToken() throws Exception { + final Service service = new SimpleWebApplicationServiceImpl("id"); final ServiceTicket serviceTicket = mock(ServiceTicket.class); @@ -514,8 +544,8 @@ public void verifyOKWithOfflineToken() throws Exception { when(centralAuthenticationService.validateServiceTicket(serviceTicket.getId(), serviceTicket.getService())).thenReturn(assertion); - final MockHttpServletRequest mockRequest = new MockHttpServletRequest("GET", CONTEXT - + OAuthConstants.PROFILE_URL); + final MockHttpServletRequest mockRequest + = new MockHttpServletRequest("GET", CONTEXT + OAuthConstants.PROFILE_URL); mockRequest.setParameter(OAuthConstants.ACCESS_TOKEN, AT_ID); final MockHttpServletResponse mockResponse = new MockHttpServletResponse(); @@ -532,8 +562,8 @@ public void verifyOKWithOfflineToken() throws Exception { final ObjectMapper mapper = new ObjectMapper(); - final String expected = "{\"id\":\"" + ID + "\",\"attributes\":[{\"" + NAME + "\":\"" + VALUE + "\"},{\"" + NAME2 - + "\":[\"" + VALUE + "\",\"" + VALUE + "\"]}]}"; + final String expected = "{\"id\":\"" + ID + "\",\"attributes\":[{\"" + NAME + + "\":\"" + VALUE + "\"},{\"" + NAME2 + "\":[\"" + VALUE + "\",\"" + VALUE + "\"]}]}"; final JsonNode expectedObj = mapper.readTree(expected); final JsonNode receivedObj = mapper.readTree(mockResponse.getContentAsString()); assertEquals(expectedObj.get("id").asText(), receivedObj.get("id").asText()); diff --git a/cas-server-support-oauth/src/test/java/org/jasig/cas/support/oauth/web/OAuth20RevokeClientPrincipalTokensControllerTests.java b/cas-server-support-oauth/src/test/java/org/jasig/cas/support/oauth/web/OAuth20RevokeClientPrincipalTokensControllerTests.java index e38544bc..2bddfdc1 100644 --- a/cas-server-support-oauth/src/test/java/org/jasig/cas/support/oauth/web/OAuth20RevokeClientPrincipalTokensControllerTests.java +++ b/cas-server-support-oauth/src/test/java/org/jasig/cas/support/oauth/web/OAuth20RevokeClientPrincipalTokensControllerTests.java @@ -19,28 +19,32 @@ package org.jasig.cas.support.oauth.web; import org.apache.http.HttpStatus; + import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; + import org.jasig.cas.support.oauth.CentralOAuthService; import org.jasig.cas.support.oauth.OAuthConstants; import org.jasig.cas.support.oauth.token.AccessToken; import org.jasig.cas.support.oauth.token.InvalidTokenException; -import org.junit.Test; -import org.springframework.mock.web.MockHttpServletRequest; -import org.springframework.mock.web.MockHttpServletResponse; -import org.springframework.web.servlet.ModelAndView; - import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; +import org.junit.Test; + import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.web.servlet.ModelAndView; + /** * This class tests the {@link OAuth20RevokeClientPrincipalTokensController} class. * * @author Fitz Elliott - * @since 3.5.2 + * @author Longze Chen + * @since 4.1.5 */ public final class OAuth20RevokeClientPrincipalTokensControllerTests { @@ -54,9 +58,11 @@ public final class OAuth20RevokeClientPrincipalTokensControllerTests { @Test public void verifyNoTokenOrAuthHeader() throws Exception { - final MockHttpServletRequest mockRequest = new MockHttpServletRequest("POST", CONTEXT - + OAuthConstants.REVOKE_URL); + + final MockHttpServletRequest mockRequest + = new MockHttpServletRequest("POST", CONTEXT + OAuthConstants.REVOKE_URL); mockRequest.setParameter(OAuthConstants.CLIENT_ID, CLIENT_ID); + final MockHttpServletResponse mockResponse = new MockHttpServletResponse(); final OAuth20WrapperController oauth20WrapperController = new OAuth20WrapperController(); @@ -67,8 +73,8 @@ public void verifyNoTokenOrAuthHeader() throws Exception { assertEquals(HttpStatus.SC_BAD_REQUEST, mockResponse.getStatus()); assertEquals(CONTENT_TYPE, mockResponse.getContentType()); - final String expected = "{\"error\":\"" + OAuthConstants.INVALID_REQUEST + "\",\"error_description\":\"" - + OAuthConstants.MISSING_ACCESS_TOKEN_DESCRIPTION + "\"}"; + final String expected = "{\"error\":\"" + OAuthConstants.INVALID_REQUEST + + "\",\"error_description\":\"" + "Invalid or missing parameter 'access_token'\"}"; final ObjectMapper mapper = new ObjectMapper(); final JsonNode expectedObj = mapper.readTree(expected); @@ -79,10 +85,12 @@ public void verifyNoTokenOrAuthHeader() throws Exception { @Test public void verifyNoTokenAndAuthHeaderIsBlank() throws Exception { - final MockHttpServletRequest mockRequest = new MockHttpServletRequest("POST", CONTEXT - + OAuthConstants.REVOKE_URL); + + final MockHttpServletRequest mockRequest + = new MockHttpServletRequest("POST", CONTEXT + OAuthConstants.REVOKE_URL); mockRequest.addHeader("Authorization", ""); mockRequest.setParameter(OAuthConstants.CLIENT_ID, CLIENT_ID); + final MockHttpServletResponse mockResponse = new MockHttpServletResponse(); final OAuth20WrapperController oauth20WrapperController = new OAuth20WrapperController(); @@ -93,8 +101,8 @@ public void verifyNoTokenAndAuthHeaderIsBlank() throws Exception { assertEquals(HttpStatus.SC_BAD_REQUEST, mockResponse.getStatus()); assertEquals(CONTENT_TYPE, mockResponse.getContentType()); - final String expected = "{\"error\":\"" + OAuthConstants.INVALID_REQUEST + "\",\"error_description\":\"" - + OAuthConstants.MISSING_ACCESS_TOKEN_DESCRIPTION + "\"}"; + final String expected = "{\"error\":\"" + OAuthConstants.INVALID_REQUEST + + "\",\"error_description\":\"" + "Invalid or missing parameter 'access_token'\"}"; final ObjectMapper mapper = new ObjectMapper(); final JsonNode expectedObj = mapper.readTree(expected); @@ -105,10 +113,12 @@ public void verifyNoTokenAndAuthHeaderIsBlank() throws Exception { @Test public void verifyNoTokenAndAuthHeaderIsMalformed() throws Exception { - final MockHttpServletRequest mockRequest = new MockHttpServletRequest("POST", CONTEXT - + OAuthConstants.REVOKE_URL); + + final MockHttpServletRequest mockRequest + = new MockHttpServletRequest("POST", CONTEXT + OAuthConstants.REVOKE_URL); mockRequest.addHeader("Authorization", "Let me in i am authorized"); mockRequest.setParameter(OAuthConstants.CLIENT_ID, CLIENT_ID); + final MockHttpServletResponse mockResponse = new MockHttpServletResponse(); final OAuth20WrapperController oauth20WrapperController = new OAuth20WrapperController(); @@ -119,8 +129,8 @@ public void verifyNoTokenAndAuthHeaderIsMalformed() throws Exception { assertEquals(HttpStatus.SC_BAD_REQUEST, mockResponse.getStatus()); assertEquals(CONTENT_TYPE, mockResponse.getContentType()); - final String expected = "{\"error\":\"" + OAuthConstants.INVALID_REQUEST + "\",\"error_description\":\"" - + OAuthConstants.MISSING_ACCESS_TOKEN_DESCRIPTION + "\"}"; + final String expected = "{\"error\":\"" + OAuthConstants.INVALID_REQUEST + + "\",\"error_description\":\"" + "Invalid or missing parameter 'access_token'\"}"; final ObjectMapper mapper = new ObjectMapper(); final JsonNode expectedObj = mapper.readTree(expected); @@ -131,14 +141,16 @@ public void verifyNoTokenAndAuthHeaderIsMalformed() throws Exception { @Test public void verifyInvalidAccessToken() throws Exception { + final CentralOAuthService centralOAuthService = mock(CentralOAuthService.class); when(centralOAuthService.getToken(AT_ID, AccessToken.class)).thenThrow(new InvalidTokenException("error")); when(centralOAuthService.getPersonalAccessToken(AT_ID)).thenReturn(null); - final MockHttpServletRequest mockRequest = new MockHttpServletRequest("GET", CONTEXT - + OAuthConstants.PROFILE_URL); + final MockHttpServletRequest mockRequest + = new MockHttpServletRequest("GET", CONTEXT + OAuthConstants.PROFILE_URL); mockRequest.setParameter(OAuthConstants.ACCESS_TOKEN, AT_ID); mockRequest.setParameter(OAuthConstants.CLIENT_ID, CLIENT_ID); + final MockHttpServletResponse mockResponse = new MockHttpServletResponse(); final OAuth20WrapperController oauth20WrapperController = new OAuth20WrapperController(); @@ -152,8 +164,8 @@ public void verifyInvalidAccessToken() throws Exception { final ObjectMapper mapper = new ObjectMapper(); - final String expected = "{\"error\":\"" + OAuthConstants.UNAUTHORIZED_REQUEST + "\",\"error_description\":\"" - + OAuthConstants.INVALID_ACCESS_TOKEN_DESCRIPTION + "\"}"; + final String expected = "{\"error\":\"" + OAuthConstants.UNAUTHORIZED_REQUEST + + "\",\"error_description\":\"" + OAuthConstants.INVALID_ACCESS_TOKEN_DESCRIPTION + "\"}"; final JsonNode expectedObj = mapper.readTree(expected); final JsonNode receivedObj = mapper.readTree(mockResponse.getContentAsString()); assertEquals(expectedObj.get("error").asText(), receivedObj.get("error").asText()); @@ -162,6 +174,7 @@ public void verifyInvalidAccessToken() throws Exception { @Test public void verifyOKWithAccessToken() throws Exception { + final AccessToken accessToken = mock(AccessToken.class); when(accessToken.getId()).thenReturn(AT_ID); @@ -169,10 +182,11 @@ public void verifyOKWithAccessToken() throws Exception { when(centralOAuthService.getToken(AT_ID, AccessToken.class)).thenReturn(accessToken); when(centralOAuthService.revokeClientPrincipalTokens(accessToken, CLIENT_ID)).thenReturn(true); - final MockHttpServletRequest mockRequest = new MockHttpServletRequest("POST", CONTEXT - + OAuthConstants.REVOKE_URL); + final MockHttpServletRequest mockRequest + = new MockHttpServletRequest("POST", CONTEXT + OAuthConstants.REVOKE_URL); mockRequest.setParameter(OAuthConstants.ACCESS_TOKEN, AT_ID); mockRequest.setParameter(OAuthConstants.CLIENT_ID, CLIENT_ID); + final MockHttpServletResponse mockResponse = new MockHttpServletResponse(); final OAuth20WrapperController oauth20WrapperController = new OAuth20WrapperController(); @@ -188,6 +202,7 @@ public void verifyOKWithAccessToken() throws Exception { @Test public void verifyOKWithAuthHeader() throws Exception { + final AccessToken accessToken = mock(AccessToken.class); when(accessToken.getId()).thenReturn(AT_ID); @@ -195,10 +210,11 @@ public void verifyOKWithAuthHeader() throws Exception { when(centralOAuthService.getToken(AT_ID, AccessToken.class)).thenReturn(accessToken); when(centralOAuthService.revokeClientPrincipalTokens(accessToken, CLIENT_ID)).thenReturn(true); - final MockHttpServletRequest mockRequest = new MockHttpServletRequest("POST", CONTEXT - + OAuthConstants.REVOKE_URL); + final MockHttpServletRequest mockRequest + = new MockHttpServletRequest("POST", CONTEXT + OAuthConstants.REVOKE_URL); mockRequest.addHeader("Authorization", OAuthConstants.BEARER_TOKEN + " " + AT_ID); mockRequest.setParameter(OAuthConstants.CLIENT_ID, CLIENT_ID); + final MockHttpServletResponse mockResponse = new MockHttpServletResponse(); final OAuth20WrapperController oauth20WrapperController = new OAuth20WrapperController(); @@ -214,6 +230,7 @@ public void verifyOKWithAuthHeader() throws Exception { @Test public void verifyFailedToRevokeTokens() throws Exception { + final AccessToken accessToken = mock(AccessToken.class); when(accessToken.getId()).thenReturn(AT_ID); @@ -221,8 +238,8 @@ public void verifyFailedToRevokeTokens() throws Exception { when(centralOAuthService.getToken(AT_ID, AccessToken.class)).thenReturn(accessToken); when(centralOAuthService.revokeClientPrincipalTokens(accessToken, CLIENT_ID)).thenReturn(false); - final MockHttpServletRequest mockRequest = new MockHttpServletRequest("POST", CONTEXT - + OAuthConstants.REVOKE_URL); + final MockHttpServletRequest mockRequest + = new MockHttpServletRequest("POST", CONTEXT + OAuthConstants.REVOKE_URL); mockRequest.setParameter(OAuthConstants.ACCESS_TOKEN, AT_ID); mockRequest.setParameter(OAuthConstants.CLIENT_ID, CLIENT_ID); final MockHttpServletResponse mockResponse = new MockHttpServletResponse(); @@ -236,8 +253,8 @@ public void verifyFailedToRevokeTokens() throws Exception { assertEquals(HttpStatus.SC_BAD_REQUEST, mockResponse.getStatus()); assertEquals(CONTENT_TYPE, mockResponse.getContentType()); - final String expected = "{\"error\":\"" + OAuthConstants.INVALID_REQUEST + "\",\"error_description\":\"" - + OAuthConstants.INVALID_ACCESS_TOKEN_DESCRIPTION + "\"}"; + final String expected = "{\"error\":\"" + OAuthConstants.INVALID_REQUEST + + "\",\"error_description\":\"" + OAuthConstants.INVALID_ACCESS_TOKEN_DESCRIPTION + "\"}"; final ObjectMapper mapper = new ObjectMapper(); final JsonNode expectedObj = mapper.readTree(expected); diff --git a/cas-server-support-oauth/src/test/java/org/jasig/cas/support/oauth/web/OAuth20RevokeClientTokensControllerTests.java b/cas-server-support-oauth/src/test/java/org/jasig/cas/support/oauth/web/OAuth20RevokeClientTokensControllerTests.java index 94022600..88e1fbb6 100644 --- a/cas-server-support-oauth/src/test/java/org/jasig/cas/support/oauth/web/OAuth20RevokeClientTokensControllerTests.java +++ b/cas-server-support-oauth/src/test/java/org/jasig/cas/support/oauth/web/OAuth20RevokeClientTokensControllerTests.java @@ -19,15 +19,24 @@ package org.jasig.cas.support.oauth.web; import org.apache.http.HttpStatus; + import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; + import org.jasig.cas.support.oauth.CentralOAuthService; import org.jasig.cas.support.oauth.OAuthConstants; import org.jasig.cas.support.oauth.services.OAuthRegisteredService; import org.jasig.cas.support.oauth.token.AccessToken; import org.jasig.cas.support.oauth.token.RefreshToken; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; import org.junit.Test; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.web.servlet.ModelAndView; @@ -35,13 +44,8 @@ import java.util.Collection; import java.util.LinkedList; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNull; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - /** - * This class tests the {@link OAuth20RevokeClienTokensController} class. + * This class tests the {@link OAuth20RevokeClientTokensController} class. * * @author Fitz Elliott * @author Longze Chen @@ -65,12 +69,14 @@ public final class OAuth20RevokeClientTokensControllerTests { /** Test that no client id raises HTTP 400 Bad Request. */ @Test public void verifyNoClientId() throws Exception { + final CentralOAuthService centralOAuthService = mock(CentralOAuthService.class); final MockHttpServletRequest mockRequest = new MockHttpServletRequest("POST", CONTEXT + OAuthConstants.REVOKE_URL); mockRequest.setParameter(OAuthConstants.CLIENT_ID, ""); mockRequest.setParameter(OAuthConstants.CLIENT_SECRET, CLIENT_SECRET); + final MockHttpServletResponse mockResponse = new MockHttpServletResponse(); final OAuth20WrapperController oauth20WrapperController = new OAuth20WrapperController(); @@ -82,8 +88,8 @@ public void verifyNoClientId() throws Exception { assertEquals(HttpStatus.SC_BAD_REQUEST, mockResponse.getStatus()); assertEquals(CONTENT_TYPE, mockResponse.getContentType()); - final String expected = "{\"error\":\"" + OAuthConstants.INVALID_REQUEST + "\",\"error_description\":\"" - + "Invalid or missing parameter 'client_id'\"}"; + final String expected = "{\"error\":\"" + OAuthConstants.INVALID_REQUEST + + "\",\"error_description\":\"" + "Invalid or missing parameter 'client_id'\"}"; final ObjectMapper mapper = new ObjectMapper(); final JsonNode expectedObj = mapper.readTree(expected); @@ -95,12 +101,14 @@ public void verifyNoClientId() throws Exception { /** Test that no client secret raises HTTP 400 Bad Request. */ @Test public void verifyNoClientSecret() throws Exception { + final CentralOAuthService centralOAuthService = mock(CentralOAuthService.class); final MockHttpServletRequest mockRequest = new MockHttpServletRequest("POST", CONTEXT + OAuthConstants.REVOKE_URL); mockRequest.setParameter(OAuthConstants.CLIENT_ID, CLIENT_ID); mockRequest.setParameter(OAuthConstants.CLIENT_SECRET, ""); + final MockHttpServletResponse mockResponse = new MockHttpServletResponse(); final OAuth20WrapperController oauth20WrapperController = new OAuth20WrapperController(); @@ -112,8 +120,8 @@ public void verifyNoClientSecret() throws Exception { assertEquals(HttpStatus.SC_BAD_REQUEST, mockResponse.getStatus()); assertEquals(CONTENT_TYPE, mockResponse.getContentType()); - final String expected = "{\"error\":\"" + OAuthConstants.INVALID_REQUEST + "\",\"error_description\":\"" - + "Invalid or missing parameter 'client_secret'\"}"; + final String expected = "{\"error\":\"" + OAuthConstants.INVALID_REQUEST + + "\",\"error_description\":\"" + "Invalid or missing parameter 'client_secret'\"}"; final ObjectMapper mapper = new ObjectMapper(); final JsonNode expectedObj = mapper.readTree(expected); @@ -125,12 +133,14 @@ public void verifyNoClientSecret() throws Exception { /** Test that client id not found raises HTTP 400 Bad Request. */ @Test public void verifyNoSuchClientId() throws Exception { + final CentralOAuthService centralOAuthService = mock(CentralOAuthService.class); final MockHttpServletRequest mockRequest = new MockHttpServletRequest("POST", CONTEXT + OAuthConstants.REVOKE_URL); mockRequest.setParameter(OAuthConstants.CLIENT_ID, NO_SUCH_CLIENT_ID); mockRequest.setParameter(OAuthConstants.CLIENT_SECRET, CLIENT_SECRET); + final MockHttpServletResponse mockResponse = new MockHttpServletResponse(); final OAuth20WrapperController oauth20WrapperController = new OAuth20WrapperController(); @@ -142,8 +152,8 @@ public void verifyNoSuchClientId() throws Exception { assertEquals(HttpStatus.SC_BAD_REQUEST, mockResponse.getStatus()); assertEquals(CONTENT_TYPE, mockResponse.getContentType()); - final String expected = "{\"error\":\"" + OAuthConstants.INVALID_REQUEST + "\",\"error_description\":\"" - + OAuthConstants.INVALID_CLIENT_ID_OR_SECRET_DESCRIPTION + "\"}"; + final String expected = "{\"error\":\"" + OAuthConstants.INVALID_REQUEST + + "\",\"error_description\":\"" + OAuthConstants.INVALID_CLIENT_ID_OR_SECRET_DESCRIPTION + "\"}"; final ObjectMapper mapper = new ObjectMapper(); final JsonNode expectedObj = mapper.readTree(expected); @@ -155,12 +165,14 @@ public void verifyNoSuchClientId() throws Exception { /** Test that wrong client secret raises HTTP 400 Bad Request. */ @Test public void verifyWrongClientSecret() throws Exception { + final CentralOAuthService centralOAuthService = mock(CentralOAuthService.class); final MockHttpServletRequest mockRequest = new MockHttpServletRequest("POST", CONTEXT + OAuthConstants.REVOKE_URL); mockRequest.setParameter(OAuthConstants.CLIENT_ID, CLIENT_ID); mockRequest.setParameter(OAuthConstants.CLIENT_SECRET, WRONG_CLIENT_SECRET); + final MockHttpServletResponse mockResponse = new MockHttpServletResponse(); final OAuth20WrapperController oauth20WrapperController = new OAuth20WrapperController(); @@ -172,8 +184,8 @@ public void verifyWrongClientSecret() throws Exception { assertEquals(HttpStatus.SC_BAD_REQUEST, mockResponse.getStatus()); assertEquals(CONTENT_TYPE, mockResponse.getContentType()); - final String expected = "{\"error\":\"" + OAuthConstants.INVALID_REQUEST + "\",\"error_description\":\"" - + OAuthConstants.INVALID_CLIENT_ID_OR_SECRET_DESCRIPTION + "\"}"; + final String expected = "{\"error\":\"" + OAuthConstants.INVALID_REQUEST + + "\",\"error_description\":\"" + OAuthConstants.INVALID_CLIENT_ID_OR_SECRET_DESCRIPTION + "\"}"; final ObjectMapper mapper = new ObjectMapper(); final JsonNode expectedObj = mapper.readTree(expected); @@ -182,9 +194,10 @@ public void verifyWrongClientSecret() throws Exception { assertEquals(expectedObj.get("error_description").asText(), receivedObj.get("error_description").asText()); } - /* Test that valid client id and secret succeeds and returns HTTP 204 No Content. */ + /** Test that valid client id and secret succeeds and returns HTTP 204 No Content. */ @Test public void verifyOK() throws Exception { + final CentralOAuthService centralOAuthService = mock(CentralOAuthService.class); final Collection accessTokens = new LinkedList<>(); final Collection refreshTokens = new LinkedList<>(); @@ -200,6 +213,7 @@ public void verifyOK() throws Exception { = new MockHttpServletRequest("POST", CONTEXT + OAuthConstants.REVOKE_URL); mockRequest.setParameter(OAuthConstants.CLIENT_ID, CLIENT_ID); mockRequest.setParameter(OAuthConstants.CLIENT_SECRET, CLIENT_SECRET); + final MockHttpServletResponse mockResponse = new MockHttpServletResponse(); final OAuth20WrapperController oauth20WrapperController = new OAuth20WrapperController(); diff --git a/cas-server-support-oauth/src/test/java/org/jasig/cas/support/oauth/web/OAuth20RevokeTokenControllerTests.java b/cas-server-support-oauth/src/test/java/org/jasig/cas/support/oauth/web/OAuth20RevokeTokenControllerTests.java index b59000de..19caef7a 100644 --- a/cas-server-support-oauth/src/test/java/org/jasig/cas/support/oauth/web/OAuth20RevokeTokenControllerTests.java +++ b/cas-server-support-oauth/src/test/java/org/jasig/cas/support/oauth/web/OAuth20RevokeTokenControllerTests.java @@ -19,28 +19,32 @@ package org.jasig.cas.support.oauth.web; import org.apache.http.HttpStatus; + import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; + import org.jasig.cas.support.oauth.CentralOAuthService; import org.jasig.cas.support.oauth.OAuthConstants; import org.jasig.cas.support.oauth.token.AccessToken; import org.jasig.cas.support.oauth.token.InvalidTokenException; -import org.junit.Test; -import org.springframework.mock.web.MockHttpServletRequest; -import org.springframework.mock.web.MockHttpServletResponse; -import org.springframework.web.servlet.ModelAndView; - import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; +import org.junit.Test; + import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.web.servlet.ModelAndView; + /** * This class tests the {@link OAuth20RevokeTokenController} class. * * @author Fitz Elliott - * @since 3.5.2 + * @author Longze Chen + * @since 4.1.5 */ public final class OAuth20RevokeTokenControllerTests { @@ -52,11 +56,13 @@ public final class OAuth20RevokeTokenControllerTests { @Test public void verifyNoToken() throws Exception { + final CentralOAuthService centralOAuthService = mock(CentralOAuthService.class); when(centralOAuthService.getToken(null)).thenThrow(new InvalidTokenException("error")); - final MockHttpServletRequest mockRequest = new MockHttpServletRequest("POST", CONTEXT - + OAuthConstants.REVOKE_URL); + final MockHttpServletRequest mockRequest + = new MockHttpServletRequest("POST", CONTEXT + OAuthConstants.REVOKE_URL); + final MockHttpServletResponse mockResponse = new MockHttpServletResponse(); final OAuth20WrapperController oauth20WrapperController = new OAuth20WrapperController(); @@ -71,7 +77,7 @@ public void verifyNoToken() throws Exception { final ObjectMapper mapper = new ObjectMapper(); final String expected = "{\"error\":\"" + OAuthConstants.INVALID_REQUEST - + "\",\"error_description\":\"Invalid Token\"}"; + + "\",\"error_description\":\"" + "Invalid or missing parameter 'token'\"}"; final JsonNode expectedObj = mapper.readTree(expected); final JsonNode receivedObj = mapper.readTree(mockResponse.getContentAsString()); assertEquals(expectedObj.get("error").asText(), receivedObj.get("error").asText()); @@ -80,6 +86,7 @@ public void verifyNoToken() throws Exception { @Test public void verifyRevocationFail() throws Exception { + final AccessToken accessToken = mock(AccessToken.class); when(accessToken.getId()).thenReturn(TOKEN_ID); @@ -87,9 +94,10 @@ public void verifyRevocationFail() throws Exception { when(centralOAuthService.getToken(TOKEN_ID)).thenReturn(accessToken); when(centralOAuthService.revokeToken(accessToken)).thenReturn(false); - final MockHttpServletRequest mockRequest = new MockHttpServletRequest("POST", CONTEXT - + OAuthConstants.REVOKE_URL); + final MockHttpServletRequest mockRequest + = new MockHttpServletRequest("POST", CONTEXT + OAuthConstants.REVOKE_URL); mockRequest.setParameter(OAuthConstants.TOKEN, TOKEN_ID); + final MockHttpServletResponse mockResponse = new MockHttpServletResponse(); final OAuth20WrapperController oauth20WrapperController = new OAuth20WrapperController(); @@ -104,7 +112,7 @@ public void verifyRevocationFail() throws Exception { final ObjectMapper mapper = new ObjectMapper(); final String expected = "{\"error\":\"" + OAuthConstants.INVALID_REQUEST - + "\",\"error_description\":\"" + OAuthConstants.FAILED_TOKEN_REVOCATION_DESCRIPTION + "\"}"; + + "\",\"error_description\":\"" + OAuthConstants.FAILED_TOKEN_REVOCATION_DESCRIPTION + "\"}"; final JsonNode expectedObj = mapper.readTree(expected); final JsonNode receivedObj = mapper.readTree(mockResponse.getContentAsString()); assertEquals(expectedObj.get("error").asText(), receivedObj.get("error").asText()); @@ -113,6 +121,7 @@ public void verifyRevocationFail() throws Exception { @Test public void verifyOK() throws Exception { + final AccessToken accessToken = mock(AccessToken.class); when(accessToken.getId()).thenReturn(TOKEN_ID); @@ -120,9 +129,10 @@ public void verifyOK() throws Exception { when(centralOAuthService.getToken(TOKEN_ID)).thenReturn(accessToken); when(centralOAuthService.revokeToken(accessToken)).thenReturn(true); - final MockHttpServletRequest mockRequest = new MockHttpServletRequest("POST", CONTEXT - + OAuthConstants.REVOKE_URL); + final MockHttpServletRequest mockRequest + = new MockHttpServletRequest("POST", CONTEXT + OAuthConstants.REVOKE_URL); mockRequest.setParameter(OAuthConstants.TOKEN, TOKEN_ID); + final MockHttpServletResponse mockResponse = new MockHttpServletResponse(); final OAuth20WrapperController oauth20WrapperController = new OAuth20WrapperController(); @@ -135,5 +145,4 @@ public void verifyOK() throws Exception { assertNull(mockResponse.getContentType()); assertEquals("null", mockResponse.getContentAsString()); } - } diff --git a/cas-server-support-oauth/src/test/java/org/jasig/cas/support/oauth/web/OAuth20ServiceValidateControllerTests.java b/cas-server-support-oauth/src/test/java/org/jasig/cas/support/oauth/web/OAuth20ServiceValidateControllerTests.java index 3d0d2996..a526efca 100644 --- a/cas-server-support-oauth/src/test/java/org/jasig/cas/support/oauth/web/OAuth20ServiceValidateControllerTests.java +++ b/cas-server-support-oauth/src/test/java/org/jasig/cas/support/oauth/web/OAuth20ServiceValidateControllerTests.java @@ -19,8 +19,9 @@ package org.jasig.cas.support.oauth.web; import org.apache.http.HttpStatus; -import org.jasig.cas.CentralAuthenticationService; + import org.jasig.cas.authentication.principal.WebApplicationService; +import org.jasig.cas.CentralAuthenticationService; import org.jasig.cas.support.oauth.CentralOAuthService; import org.jasig.cas.support.oauth.OAuthConstants; import org.jasig.cas.support.oauth.token.AccessToken; @@ -31,7 +32,15 @@ import org.jasig.cas.ticket.TicketCreationException; import org.jasig.cas.web.support.ArgumentExtractor; import org.jasig.cas.validation.Assertion; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; import org.junit.Test; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.web.context.WebApplicationContext; @@ -41,12 +50,6 @@ import java.util.Map; import java.util.Set; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - /** * This class tests the {@link OAuth20ServiceValidateController} class. * @@ -55,24 +58,31 @@ */ public final class OAuth20ServiceValidateControllerTests { - public static final String URL = "/p3/serviceValidate"; + private static final String URL = "/p3/serviceValidate"; - public static final String SERVICE_TICKET_ID = "ST-1"; + private static final String SERVICE_TICKET_ID = "ST-1"; - public static final String AT_ID = "AT-1"; + private static final String AT_ID = "AT-1"; - public static final String SCOPE1 = "user1"; + private static final String SCOPE1 = "user1"; - public static final String SCOPE2 = "user2"; + private static final String SCOPE2 = "user2"; - public static final String SUCCESS_VIEW = "cas2ServiceSuccessView"; + private static final String SUCCESS_VIEW = "cas2ServiceSuccessView"; @Test public void verifyHandleInternal() throws Exception { + final MockHttpServletRequest mockRequest = new MockHttpServletRequest("GET", URL); + final MockHttpServletResponse mockResponse = new MockHttpServletResponse(); - final OAuth20ServiceValidateController oauth20ServiceValidateController = new OAuth20ServiceValidateController(); - final ModelAndView modelAndView = oauth20ServiceValidateController.handleRequestInternal(mockRequest, mockResponse); + + final OAuth20ServiceValidateController oauth20ServiceValidateController + = new OAuth20ServiceValidateController(); + + final ModelAndView modelAndView + = oauth20ServiceValidateController.handleRequestInternal(mockRequest, mockResponse); + assertNull(modelAndView); assertEquals(HttpStatus.SC_OK, mockResponse.getStatus()); assertEquals("", mockResponse.getContentAsString()); @@ -80,6 +90,7 @@ public void verifyHandleInternal() throws Exception { @Test public void verifyGetTicketException() throws Exception { + final MockHttpServletRequest mockRequest = new MockHttpServletRequest("GET", URL); final WebApplicationService webApplicationService = mock(WebApplicationService.class); @@ -97,9 +108,10 @@ public void verifyGetTicketException() throws Exception { final Assertion assertion = mock(Assertion.class); final CentralAuthenticationService centralAuthenticationService = mock(CentralAuthenticationService.class); - when(centralAuthenticationService.getTicket(SERVICE_TICKET_ID, Ticket.class)).thenThrow(new InvalidTicketException("weak")); - when(centralAuthenticationService.validateServiceTicket(SERVICE_TICKET_ID, webApplicationService)).thenReturn(assertion); - + when(centralAuthenticationService.getTicket(SERVICE_TICKET_ID, Ticket.class)) + .thenThrow(new InvalidTicketException("weak")); + when(centralAuthenticationService.validateServiceTicket(SERVICE_TICKET_ID, webApplicationService)) + .thenReturn(assertion); final Set scopes = new HashSet<>(); scopes.add(SCOPE1); @@ -110,21 +122,25 @@ public void verifyGetTicketException() throws Exception { when(accessToken.getScopes()).thenReturn(scopes); final CentralOAuthService centralOAuthService = mock(CentralOAuthService.class); - when(centralOAuthService.grantCASAccessToken(ticketGrantingTicket, webApplicationService)).thenReturn(accessToken); + when(centralOAuthService.grantCASAccessToken(ticketGrantingTicket, webApplicationService)) + .thenReturn(accessToken); final MockHttpServletResponse mockResponse = new MockHttpServletResponse(); - final OAuth20ServiceValidateController oauth20ServiceValidateController = new OAuth20ServiceValidateController(); + + final OAuth20ServiceValidateController oauth20ServiceValidateController + = new OAuth20ServiceValidateController(); oauth20ServiceValidateController.setArgumentExtractor(argumentExtractor); oauth20ServiceValidateController.setCentralAuthenticationService(centralAuthenticationService); oauth20ServiceValidateController.setCentralOAuthService(centralOAuthService); oauth20ServiceValidateController.setSuccessView(SUCCESS_VIEW); + final ModelAndView modelAndView = oauth20ServiceValidateController.handleRequest(mockRequest, mockResponse); assertEquals(HttpStatus.SC_OK, mockResponse.getStatus()); assertEquals("", mockResponse.getContentAsString()); final Map model = modelAndView.getModel(); - assertTrue(!model.containsKey(OAuthConstants.CAS_PROTOCOL_ACCESS_TOKEN)); - assertTrue(!model.containsKey(OAuthConstants.CAS_PROTOCOL_ACCESS_TOKEN_SCOPE)); + assertFalse(model.containsKey(OAuthConstants.CAS_PROTOCOL_ACCESS_TOKEN)); + assertFalse(model.containsKey(OAuthConstants.CAS_PROTOCOL_ACCESS_TOKEN_SCOPE)); } @Test @@ -182,6 +198,7 @@ public void verifyOK() throws Exception { @Test public void verifyBypassCASWithNoService() throws Exception { + final MockHttpServletRequest mockRequest = new MockHttpServletRequest("GET", URL); final WebApplicationService webApplicationService = mock(WebApplicationService.class); @@ -200,7 +217,8 @@ public void verifyBypassCASWithNoService() throws Exception { final CentralAuthenticationService centralAuthenticationService = mock(CentralAuthenticationService.class); when(centralAuthenticationService.getTicket(SERVICE_TICKET_ID, Ticket.class)).thenReturn(serviceTicket); - when(centralAuthenticationService.validateServiceTicket(SERVICE_TICKET_ID, webApplicationService)).thenReturn(assertion); + when(centralAuthenticationService.validateServiceTicket(SERVICE_TICKET_ID, webApplicationService)) + .thenReturn(assertion); final Set scopes = new HashSet<>(); scopes.add(SCOPE1); @@ -211,11 +229,13 @@ public void verifyBypassCASWithNoService() throws Exception { when(accessToken.getScopes()).thenReturn(scopes); final CentralOAuthService centralOAuthService = mock(CentralOAuthService.class); - when(centralOAuthService.grantCASAccessToken(ticketGrantingTicket, webApplicationService)).thenReturn(accessToken); + when(centralOAuthService.grantCASAccessToken(ticketGrantingTicket, webApplicationService)) + .thenReturn(accessToken); final WebApplicationContext webApplicationContext = mock(WebApplicationContext.class); - final OAuth20ServiceValidateController oauth20ServiceValidateController = new OAuth20ServiceValidateController(); + final OAuth20ServiceValidateController oauth20ServiceValidateController + = new OAuth20ServiceValidateController(); oauth20ServiceValidateController.initApplicationContext(webApplicationContext); oauth20ServiceValidateController.setArgumentExtractor(argumentExtractor); oauth20ServiceValidateController.setCentralAuthenticationService(centralAuthenticationService); @@ -229,12 +249,13 @@ public void verifyBypassCASWithNoService() throws Exception { assertEquals("", mockResponse.getContentAsString()); final Map model = modelAndView.getModel(); - assertTrue(!model.containsKey(OAuthConstants.CAS_PROTOCOL_ACCESS_TOKEN)); - assertTrue(!model.containsKey(OAuthConstants.CAS_PROTOCOL_ACCESS_TOKEN_SCOPE)); + assertFalse(model.containsKey(OAuthConstants.CAS_PROTOCOL_ACCESS_TOKEN)); + assertFalse(model.containsKey(OAuthConstants.CAS_PROTOCOL_ACCESS_TOKEN_SCOPE)); } @Test public void verifyBypassCASWithNoServiceTicketWrongSuccessView() throws Exception { + final MockHttpServletRequest mockRequest = new MockHttpServletRequest("GET", URL); final WebApplicationService webApplicationService = mock(WebApplicationService.class); @@ -253,7 +274,8 @@ public void verifyBypassCASWithNoServiceTicketWrongSuccessView() throws Exceptio final CentralAuthenticationService centralAuthenticationService = mock(CentralAuthenticationService.class); when(centralAuthenticationService.getTicket(SERVICE_TICKET_ID, Ticket.class)).thenReturn(null); - when(centralAuthenticationService.validateServiceTicket(SERVICE_TICKET_ID, webApplicationService)).thenReturn(assertion); + when(centralAuthenticationService.validateServiceTicket(SERVICE_TICKET_ID, webApplicationService)) + .thenReturn(assertion); final Set scopes = new HashSet<>(); scopes.add(SCOPE1); @@ -264,11 +286,13 @@ public void verifyBypassCASWithNoServiceTicketWrongSuccessView() throws Exceptio when(accessToken.getScopes()).thenReturn(scopes); final CentralOAuthService centralOAuthService = mock(CentralOAuthService.class); - when(centralOAuthService.grantCASAccessToken(ticketGrantingTicket, webApplicationService)).thenReturn(accessToken); + when(centralOAuthService.grantCASAccessToken(ticketGrantingTicket, webApplicationService)) + .thenReturn(accessToken); final WebApplicationContext webApplicationContext = mock(WebApplicationContext.class); - final OAuth20ServiceValidateController oauth20ServiceValidateController = new OAuth20ServiceValidateController(); + final OAuth20ServiceValidateController oauth20ServiceValidateController + = new OAuth20ServiceValidateController(); oauth20ServiceValidateController.initApplicationContext(webApplicationContext); oauth20ServiceValidateController.setArgumentExtractor(argumentExtractor); oauth20ServiceValidateController.setCentralAuthenticationService(centralAuthenticationService); @@ -282,12 +306,13 @@ public void verifyBypassCASWithNoServiceTicketWrongSuccessView() throws Exceptio assertEquals("", mockResponse.getContentAsString()); final Map model = modelAndView.getModel(); - assertTrue(!model.containsKey(OAuthConstants.CAS_PROTOCOL_ACCESS_TOKEN)); - assertTrue(!model.containsKey(OAuthConstants.CAS_PROTOCOL_ACCESS_TOKEN_SCOPE)); + assertFalse(model.containsKey(OAuthConstants.CAS_PROTOCOL_ACCESS_TOKEN)); + assertFalse(model.containsKey(OAuthConstants.CAS_PROTOCOL_ACCESS_TOKEN_SCOPE)); } @Test public void verifyBypassCASWithWrongSuccessView() throws Exception { + final MockHttpServletRequest mockRequest = new MockHttpServletRequest("GET", URL); final WebApplicationService webApplicationService = mock(WebApplicationService.class); @@ -302,8 +327,6 @@ public void verifyBypassCASWithWrongSuccessView() throws Exception { when(serviceTicket.getGrantingTicket()).thenReturn(ticketGrantingTicket); when(serviceTicket.getService()).thenReturn(webApplicationService); - final Assertion assertion = mock(Assertion.class); - final CentralAuthenticationService centralAuthenticationService = mock(CentralAuthenticationService.class); when(centralAuthenticationService.getTicket(SERVICE_TICKET_ID, Ticket.class)).thenReturn(serviceTicket); when(centralAuthenticationService.validateServiceTicket(SERVICE_TICKET_ID, webApplicationService)) @@ -318,11 +341,13 @@ public void verifyBypassCASWithWrongSuccessView() throws Exception { when(accessToken.getScopes()).thenReturn(scopes); final CentralOAuthService centralOAuthService = mock(CentralOAuthService.class); - when(centralOAuthService.grantCASAccessToken(ticketGrantingTicket, webApplicationService)).thenReturn(accessToken); + when(centralOAuthService.grantCASAccessToken(ticketGrantingTicket, webApplicationService)) + .thenReturn(accessToken); final WebApplicationContext webApplicationContext = mock(WebApplicationContext.class); - final OAuth20ServiceValidateController oauth20ServiceValidateController = new OAuth20ServiceValidateController(); + final OAuth20ServiceValidateController oauth20ServiceValidateController + = new OAuth20ServiceValidateController(); oauth20ServiceValidateController.initApplicationContext(webApplicationContext); oauth20ServiceValidateController.setArgumentExtractor(argumentExtractor); oauth20ServiceValidateController.setCentralAuthenticationService(centralAuthenticationService); @@ -336,7 +361,7 @@ public void verifyBypassCASWithWrongSuccessView() throws Exception { assertEquals("", mockResponse.getContentAsString()); final Map model = modelAndView.getModel(); - assertTrue(!model.containsKey(OAuthConstants.CAS_PROTOCOL_ACCESS_TOKEN)); - assertTrue(!model.containsKey(OAuthConstants.CAS_PROTOCOL_ACCESS_TOKEN_SCOPE)); + assertFalse(model.containsKey(OAuthConstants.CAS_PROTOCOL_ACCESS_TOKEN)); + assertFalse(model.containsKey(OAuthConstants.CAS_PROTOCOL_ACCESS_TOKEN_SCOPE)); } } diff --git a/cas-server-support-oauth/src/test/java/org/jasig/cas/support/oauth/web/OAuth20TokenAuthorizationCodeControllerTests.java b/cas-server-support-oauth/src/test/java/org/jasig/cas/support/oauth/web/OAuth20TokenAuthorizationCodeControllerTests.java index d08d6eb7..da283188 100644 --- a/cas-server-support-oauth/src/test/java/org/jasig/cas/support/oauth/web/OAuth20TokenAuthorizationCodeControllerTests.java +++ b/cas-server-support-oauth/src/test/java/org/jasig/cas/support/oauth/web/OAuth20TokenAuthorizationCodeControllerTests.java @@ -18,13 +18,11 @@ */ package org.jasig.cas.support.oauth.web; -import static org.junit.Assert.*; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; + import org.apache.http.HttpStatus; + import org.jasig.cas.support.oauth.CentralOAuthService; import org.jasig.cas.support.oauth.InvalidParameterException; import org.jasig.cas.support.oauth.OAuthConstants; @@ -35,7 +33,15 @@ import org.jasig.cas.support.oauth.token.RefreshToken; import org.jasig.cas.support.oauth.token.TokenType; import org.jasig.cas.ticket.ServiceTicket; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; import org.junit.Test; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.web.servlet.ModelAndView; @@ -78,8 +84,9 @@ public final class OAuth20TokenAuthorizationCodeControllerTests { @Test public void verifyNoCode() throws Exception { - final MockHttpServletRequest mockRequest = new MockHttpServletRequest("POST", CONTEXT - + OAuthConstants.TOKEN_URL); + + final MockHttpServletRequest mockRequest + = new MockHttpServletRequest("POST", CONTEXT + OAuthConstants.TOKEN_URL); mockRequest.setParameter(OAuthConstants.GRANT_TYPE, OAuthConstants.AUTHORIZATION_CODE); mockRequest.setParameter(OAuthConstants.CLIENT_ID, CLIENT_ID); mockRequest.setParameter(OAuthConstants.CLIENT_SECRET, CLIENT_SECRET); @@ -96,9 +103,11 @@ public void verifyNoCode() throws Exception { assertEquals("application/json", mockResponse.getContentType()); final ObjectMapper mapper = new ObjectMapper(); - - final String expected = "{\"error\":\"" + OAuthConstants.INVALID_REQUEST + "\",\"error_description\":\"" - + new InvalidParameterException(OAuthConstants.CODE).getMessage() + "\"}"; + final String expected = "{" + + "\"error\":\"" + OAuthConstants.INVALID_REQUEST + "\"," + + "\"error_description\":" + + "\"" + new InvalidParameterException(OAuthConstants.CODE).getMessage() + "\"" + + "}"; final JsonNode expectedObj = mapper.readTree(expected); final JsonNode receivedObj = mapper.readTree(mockResponse.getContentAsString()); assertEquals(expectedObj.get("error").asText(), receivedObj.get("error").asText()); @@ -107,8 +116,9 @@ public void verifyNoCode() throws Exception { @Test public void verifyNoClientId() throws Exception { - final MockHttpServletRequest mockRequest = new MockHttpServletRequest("POST", CONTEXT - + OAuthConstants.TOKEN_URL); + + final MockHttpServletRequest mockRequest + = new MockHttpServletRequest("POST", CONTEXT + OAuthConstants.TOKEN_URL); mockRequest.setParameter(OAuthConstants.GRANT_TYPE, OAuthConstants.AUTHORIZATION_CODE); mockRequest.setParameter(OAuthConstants.CODE, CODE); mockRequest.setParameter(OAuthConstants.CLIENT_SECRET, CLIENT_SECRET); @@ -125,9 +135,11 @@ public void verifyNoClientId() throws Exception { assertEquals("application/json", mockResponse.getContentType()); final ObjectMapper mapper = new ObjectMapper(); - - final String expected = "{\"error\":\"" + OAuthConstants.INVALID_REQUEST + "\",\"error_description\":\"" - + new InvalidParameterException(OAuthConstants.CLIENT_ID).getMessage() + "\"}"; + final String expected = "{" + + "\"error\":\"" + OAuthConstants.INVALID_REQUEST + "\"," + + "\"error_description\":" + + "\"" + new InvalidParameterException(OAuthConstants.CLIENT_ID).getMessage() + "\"" + + "}"; final JsonNode expectedObj = mapper.readTree(expected); final JsonNode receivedObj = mapper.readTree(mockResponse.getContentAsString()); assertEquals(expectedObj.get("error").asText(), receivedObj.get("error").asText()); @@ -136,8 +148,9 @@ public void verifyNoClientId() throws Exception { @Test public void verifyNoClientSecret() throws Exception { - final MockHttpServletRequest mockRequest = new MockHttpServletRequest("POST", CONTEXT - + OAuthConstants.TOKEN_URL); + + final MockHttpServletRequest mockRequest + = new MockHttpServletRequest("POST", CONTEXT + OAuthConstants.TOKEN_URL); mockRequest.setParameter(OAuthConstants.GRANT_TYPE, OAuthConstants.AUTHORIZATION_CODE); mockRequest.setParameter(OAuthConstants.CODE, CODE); mockRequest.setParameter(OAuthConstants.CLIENT_ID, CLIENT_ID); @@ -154,9 +167,11 @@ public void verifyNoClientSecret() throws Exception { assertEquals("application/json", mockResponse.getContentType()); final ObjectMapper mapper = new ObjectMapper(); - - final String expected = "{\"error\":\"" + OAuthConstants.INVALID_REQUEST + "\",\"error_description\":\"" - + new InvalidParameterException(OAuthConstants.CLIENT_SECRET).getMessage() + "\"}"; + final String expected = "{" + + "\"error\":\"" + OAuthConstants.INVALID_REQUEST + "\"," + + "\"error_description\":" + + "\"" + new InvalidParameterException(OAuthConstants.CLIENT_SECRET).getMessage() + "\"" + + "}"; final JsonNode expectedObj = mapper.readTree(expected); final JsonNode receivedObj = mapper.readTree(mockResponse.getContentAsString()); assertEquals(expectedObj.get("error").asText(), receivedObj.get("error").asText()); @@ -165,8 +180,9 @@ public void verifyNoClientSecret() throws Exception { @Test public void verifyNoRedirectUri() throws Exception { - final MockHttpServletRequest mockRequest = new MockHttpServletRequest("POST", CONTEXT - + OAuthConstants.TOKEN_URL); + + final MockHttpServletRequest mockRequest + = new MockHttpServletRequest("POST", CONTEXT + OAuthConstants.TOKEN_URL); mockRequest.setParameter(OAuthConstants.GRANT_TYPE, OAuthConstants.AUTHORIZATION_CODE); mockRequest.setParameter(OAuthConstants.CODE, CODE); mockRequest.setParameter(OAuthConstants.CLIENT_ID, CLIENT_ID); @@ -183,9 +199,11 @@ public void verifyNoRedirectUri() throws Exception { assertEquals("application/json", mockResponse.getContentType()); final ObjectMapper mapper = new ObjectMapper(); - - final String expected = "{\"error\":\"" + OAuthConstants.INVALID_REQUEST + "\",\"error_description\":\"" - + new InvalidParameterException(OAuthConstants.REDIRECT_URI).getMessage() + "\"}"; + final String expected = "{" + + "\"error\":\"" + OAuthConstants.INVALID_REQUEST + "\"," + + "\"error_description\":" + + "\"" + new InvalidParameterException(OAuthConstants.REDIRECT_URI).getMessage() + "\"" + + "}"; final JsonNode expectedObj = mapper.readTree(expected); final JsonNode receivedObj = mapper.readTree(mockResponse.getContentAsString()); assertEquals(expectedObj.get("error").asText(), receivedObj.get("error").asText()); @@ -194,11 +212,12 @@ public void verifyNoRedirectUri() throws Exception { @Test public void verifyNoAuthorizationCode() throws Exception { + final CentralOAuthService centralOAuthService = mock(CentralOAuthService.class); when(centralOAuthService.getToken(CODE, AuthorizationCode.class)).thenThrow(new InvalidTokenException("error")); - final MockHttpServletRequest mockRequest = new MockHttpServletRequest("POST", CONTEXT - + OAuthConstants.TOKEN_URL); + final MockHttpServletRequest mockRequest + = new MockHttpServletRequest("POST", CONTEXT + OAuthConstants.TOKEN_URL); mockRequest.setParameter(OAuthConstants.GRANT_TYPE, OAuthConstants.AUTHORIZATION_CODE); mockRequest.setParameter(OAuthConstants.CODE, CODE); mockRequest.setParameter(OAuthConstants.CLIENT_ID, CLIENT_ID); @@ -217,9 +236,10 @@ public void verifyNoAuthorizationCode() throws Exception { assertEquals("application/json", mockResponse.getContentType()); final ObjectMapper mapper = new ObjectMapper(); - - final String expected = "{\"error\":\"" + OAuthConstants.INVALID_REQUEST + "\",\"error_description\":\"" - + OAuthConstants.INVALID_CODE_DESCRIPTION + "\"}"; + final String expected = "{" + + "\"error\":\"" + OAuthConstants.INVALID_REQUEST + "\"," + + "\"error_description\":\"" + OAuthConstants.INVALID_CODE_DESCRIPTION + "\"" + + "}"; final JsonNode expectedObj = mapper.readTree(expected); final JsonNode receivedObj = mapper.readTree(mockResponse.getContentAsString()); assertEquals(expectedObj.get("error").asText(), receivedObj.get("error").asText()); @@ -228,14 +248,15 @@ public void verifyNoAuthorizationCode() throws Exception { @Test public void verifyNoRegisteredService() throws Exception { + final AuthorizationCode authorizationCode = mock(AuthorizationCode.class); final CentralOAuthService centralOAuthService = mock(CentralOAuthService.class); when(centralOAuthService.getToken(CODE, AuthorizationCode.class)).thenReturn(authorizationCode); when(centralOAuthService.getRegisteredService(CLIENT_ID)).thenReturn(null); - final MockHttpServletRequest mockRequest = new MockHttpServletRequest("POST", CONTEXT - + OAuthConstants.TOKEN_URL); + final MockHttpServletRequest mockRequest + = new MockHttpServletRequest("POST", CONTEXT + OAuthConstants.TOKEN_URL); mockRequest.setParameter(OAuthConstants.GRANT_TYPE, OAuthConstants.AUTHORIZATION_CODE); mockRequest.setParameter(OAuthConstants.CODE, CODE); mockRequest.setParameter(OAuthConstants.CLIENT_ID, CLIENT_ID); @@ -254,9 +275,10 @@ public void verifyNoRegisteredService() throws Exception { assertEquals("application/json", mockResponse.getContentType()); final ObjectMapper mapper = new ObjectMapper(); - - final String expected = "{\"error\":\"" + OAuthConstants.INVALID_REQUEST + "\",\"error_description\":\"" - + OAuthConstants.INVALID_CLIENT_ID_OR_SECRET_DESCRIPTION + "\"}"; + final String expected = "{" + + "\"error\":\"" + OAuthConstants.INVALID_REQUEST + "\"," + + "\"error_description\":\"" + OAuthConstants.INVALID_CLIENT_ID_OR_SECRET_DESCRIPTION + "\"" + + "}"; final JsonNode expectedObj = mapper.readTree(expected); final JsonNode receivedObj = mapper.readTree(mockResponse.getContentAsString()); assertEquals(expectedObj.get("error").asText(), receivedObj.get("error").asText()); @@ -265,6 +287,7 @@ public void verifyNoRegisteredService() throws Exception { @Test public void verifyWrongSecret() throws Exception { + final AuthorizationCode authorizationCode = mock(AuthorizationCode.class); final OAuthRegisteredService service = getRegisteredService(REDIRECT_URI, CLIENT_SECRET); @@ -273,8 +296,8 @@ public void verifyWrongSecret() throws Exception { when(centralOAuthService.getToken(CODE, AuthorizationCode.class)).thenReturn(authorizationCode); when(centralOAuthService.getRegisteredService(CLIENT_ID)).thenReturn(service); - final MockHttpServletRequest mockRequest = new MockHttpServletRequest("POST", CONTEXT - + OAuthConstants.TOKEN_URL); + final MockHttpServletRequest mockRequest + = new MockHttpServletRequest("POST", CONTEXT + OAuthConstants.TOKEN_URL); mockRequest.setParameter(OAuthConstants.GRANT_TYPE, OAuthConstants.AUTHORIZATION_CODE); mockRequest.setParameter(OAuthConstants.CODE, CODE); mockRequest.setParameter(OAuthConstants.CLIENT_ID, CLIENT_ID); @@ -293,9 +316,8 @@ public void verifyWrongSecret() throws Exception { assertEquals("application/json", mockResponse.getContentType()); final ObjectMapper mapper = new ObjectMapper(); - - final String expected = "{\"error\":\"" + OAuthConstants.INVALID_REQUEST + "\",\"error_description\":\"" - + OAuthConstants.INVALID_CLIENT_ID_OR_SECRET_DESCRIPTION + "\"}"; + final String expected = "{\"error\":\"" + OAuthConstants.INVALID_REQUEST + + "\",\"error_description\":\"" + OAuthConstants.INVALID_CLIENT_ID_OR_SECRET_DESCRIPTION + "\"}"; final JsonNode expectedObj = mapper.readTree(expected); final JsonNode receivedObj = mapper.readTree(mockResponse.getContentAsString()); assertEquals(expectedObj.get("error").asText(), receivedObj.get("error").asText()); @@ -304,6 +326,7 @@ public void verifyWrongSecret() throws Exception { @Test public void verifyRedirectUriDoesNotStartWithServiceId() throws Exception { + final AuthorizationCode authorizationCode = mock(AuthorizationCode.class); final OAuthRegisteredService service = getRegisteredService(REDIRECT_URI, CLIENT_SECRET); @@ -312,8 +335,8 @@ public void verifyRedirectUriDoesNotStartWithServiceId() throws Exception { when(centralOAuthService.getToken(CODE, AuthorizationCode.class)).thenReturn(authorizationCode); when(centralOAuthService.getRegisteredService(CLIENT_ID)).thenReturn(service); - final MockHttpServletRequest mockRequest = new MockHttpServletRequest("POST", CONTEXT - + OAuthConstants.TOKEN_URL); + final MockHttpServletRequest mockRequest + = new MockHttpServletRequest("POST", CONTEXT + OAuthConstants.TOKEN_URL); mockRequest.setParameter(OAuthConstants.GRANT_TYPE, OAuthConstants.AUTHORIZATION_CODE); mockRequest.setParameter(OAuthConstants.CODE, CODE); mockRequest.setParameter(OAuthConstants.CLIENT_ID, CLIENT_ID); @@ -332,9 +355,8 @@ public void verifyRedirectUriDoesNotStartWithServiceId() throws Exception { assertEquals("application/json", mockResponse.getContentType()); final ObjectMapper mapper = new ObjectMapper(); - - final String expected = "{\"error\":\"" + OAuthConstants.INVALID_REQUEST + "\",\"error_description\":\"" - + OAuthConstants.INVALID_REDIRECT_URI_DESCRIPTION + "\"}"; + final String expected = "{\"error\":\"" + OAuthConstants.INVALID_REQUEST + + "\",\"error_description\":\"" + OAuthConstants.INVALID_REDIRECT_URI_DESCRIPTION + "\"}"; final JsonNode expectedObj = mapper.readTree(expected); final JsonNode receivedObj = mapper.readTree(mockResponse.getContentAsString()); assertEquals(expectedObj.get("error").asText(), receivedObj.get("error").asText()); @@ -343,6 +365,7 @@ public void verifyRedirectUriDoesNotStartWithServiceId() throws Exception { @Test public void verifyInvalidGrantType() throws Exception { + final AuthorizationCode authorizationCode = mock(AuthorizationCode.class); when(authorizationCode.getType()).thenReturn(TokenType.PERSONAL); @@ -352,8 +375,8 @@ public void verifyInvalidGrantType() throws Exception { when(centralOAuthService.getToken(CODE, AuthorizationCode.class)).thenReturn(authorizationCode); when(centralOAuthService.getRegisteredService(CLIENT_ID)).thenReturn(service); - final MockHttpServletRequest mockRequest = new MockHttpServletRequest("POST", CONTEXT - + OAuthConstants.TOKEN_URL); + final MockHttpServletRequest mockRequest + = new MockHttpServletRequest("POST", CONTEXT + OAuthConstants.TOKEN_URL); mockRequest.setParameter(OAuthConstants.GRANT_TYPE, OAuthConstants.AUTHORIZATION_CODE); mockRequest.setParameter(OAuthConstants.CODE, CODE); mockRequest.setParameter(OAuthConstants.CLIENT_ID, CLIENT_ID); @@ -372,9 +395,8 @@ public void verifyInvalidGrantType() throws Exception { assertEquals("application/json", mockResponse.getContentType()); final ObjectMapper mapper = new ObjectMapper(); - - final String expected = "{\"error\":\"" + OAuthConstants.INVALID_GRANT + "\",\"error_description\":\"" - + OAuthConstants.INVALID_GRANT_TYPE_DESCRIPTION + "\"}"; + final String expected = "{\"error\":\"" + OAuthConstants.INVALID_GRANT + + "\",\"error_description\":\"" + OAuthConstants.INVALID_GRANT_TYPE_DESCRIPTION + "\"}"; final JsonNode expectedObj = mapper.readTree(expected); final JsonNode receivedObj = mapper.readTree(mockResponse.getContentAsString()); assertEquals(expectedObj.get("error").asText(), receivedObj.get("error").asText()); @@ -383,6 +405,7 @@ public void verifyInvalidGrantType() throws Exception { @Test public void verifyOfflineOK() throws Exception { + final ServiceTicket serviceTicket = mock(ServiceTicket.class); when(serviceTicket.getCreationTime()).thenReturn(new Date().getTime()); @@ -405,8 +428,8 @@ public void verifyOfflineOK() throws Exception { when(centralOAuthService.grantOfflineRefreshToken(authorizationCode, REDIRECT_URI)).thenReturn(refreshToken); when(centralOAuthService.grantOfflineAccessToken(refreshToken)).thenReturn(accessToken); - final MockHttpServletRequest mockRequest = new MockHttpServletRequest("POST", CONTEXT - + OAuthConstants.TOKEN_URL); + final MockHttpServletRequest mockRequest + = new MockHttpServletRequest("POST", CONTEXT + OAuthConstants.TOKEN_URL); mockRequest.setParameter(OAuthConstants.GRANT_TYPE, OAuthConstants.AUTHORIZATION_CODE); mockRequest.setParameter(OAuthConstants.CODE, CODE); mockRequest.setParameter(OAuthConstants.CLIENT_ID, CLIENT_ID); @@ -426,19 +449,26 @@ public void verifyOfflineOK() throws Exception { assertEquals("application/json", mockResponse.getContentType()); final ObjectMapper mapper = new ObjectMapper(); - final String expected = "{\"token_type\":\"" + OAuthConstants.BEARER_TOKEN + "\",\"expires_in\":\"" + TIMEOUT - + "\",\"refresh_token\":\"" + RT_ID + "\",\"access_token\":\"" + AT_ID + "\"}"; + final String expected = "{" + + "\"token_type\":\"" + OAuthConstants.BEARER_TOKEN + "\"," + + "\"expires_in\":\"" + TIMEOUT + "\"," + + "\"refresh_token\":\"" + RT_ID + "\"," + + "\"access_token\":\"" + AT_ID + "\"" + + "}"; final JsonNode expectedObj = mapper.readTree(expected); final JsonNode receivedObj = mapper.readTree(mockResponse.getContentAsString()); assertEquals(expectedObj.get("token_type").asText(), receivedObj.get("token_type").asText()); - assertTrue("received expires_at greater or equal to expected", - expectedObj.get("expires_in").asInt() >= receivedObj.get("expires_in").asInt()); + assertTrue( + "received expires_at greater or equal to expected", + expectedObj.get("expires_in").asInt() >= receivedObj.get("expires_in").asInt() + ); assertEquals(expectedObj.get("refresh_token").asText(), receivedObj.get("refresh_token").asText()); assertEquals(expectedObj.get("access_token").asText(), receivedObj.get("access_token").asText()); } @Test public void verifyOnlineOK() throws Exception { + final ServiceTicket serviceTicket = mock(ServiceTicket.class); when(serviceTicket.getCreationTime()).thenReturn(new Date().getTime()); @@ -457,8 +487,8 @@ public void verifyOnlineOK() throws Exception { when(centralOAuthService.getRegisteredService(CLIENT_ID)).thenReturn(service); when(centralOAuthService.grantOnlineAccessToken(authorizationCode)).thenReturn(accessToken); - final MockHttpServletRequest mockRequest = new MockHttpServletRequest("POST", CONTEXT - + OAuthConstants.TOKEN_URL); + final MockHttpServletRequest mockRequest + = new MockHttpServletRequest("POST", CONTEXT + OAuthConstants.TOKEN_URL); mockRequest.setParameter(OAuthConstants.GRANT_TYPE, OAuthConstants.AUTHORIZATION_CODE); mockRequest.setParameter(OAuthConstants.CODE, CODE); mockRequest.setParameter(OAuthConstants.CLIENT_ID, CLIENT_ID); @@ -478,8 +508,11 @@ public void verifyOnlineOK() throws Exception { assertEquals("application/json", mockResponse.getContentType()); final ObjectMapper mapper = new ObjectMapper(); - final String expected = "{\"token_type\":\"" + OAuthConstants.BEARER_TOKEN + "\",\"expires_in\":\"" + TIMEOUT - + "\",\"access_token\":\"" + AT_ID + "\"}"; + final String expected = "{" + + "\"token_type\":\"" + OAuthConstants.BEARER_TOKEN + "\"," + + "\"expires_in\":\"" + TIMEOUT + "\"," + + "\"access_token\":\"" + AT_ID + "\"" + + "}"; final JsonNode expectedObj = mapper.readTree(expected); final JsonNode receivedObj = mapper.readTree(mockResponse.getContentAsString()); assertEquals(expectedObj.get("token_type").asText(), receivedObj.get("token_type").asText()); @@ -488,11 +521,10 @@ public void verifyOnlineOK() throws Exception { assertEquals(expectedObj.get("access_token").asText(), receivedObj.get("access_token").asText()); } - /** - * Verify that attempts to use regex for redirect URI matching fails with expected exception. - */ + /** Verify that attempts to use regex for redirect URI matching fails with expected exception. */ @Test public void verifyRegexRedirectUriDoesNotStartWithServiceId() throws Exception { + final AuthorizationCode authorizationCode = mock(AuthorizationCode.class); final OAuthRegisteredService service = getRegisteredService(REGEX_REDIRECT_URI, CLIENT_SECRET); @@ -501,8 +533,8 @@ public void verifyRegexRedirectUriDoesNotStartWithServiceId() throws Exception { when(centralOAuthService.getToken(CODE, AuthorizationCode.class)).thenReturn(authorizationCode); when(centralOAuthService.getRegisteredService(CLIENT_ID)).thenReturn(service); - final MockHttpServletRequest mockRequest = new MockHttpServletRequest("POST", CONTEXT - + OAuthConstants.TOKEN_URL); + final MockHttpServletRequest mockRequest + = new MockHttpServletRequest("POST", CONTEXT + OAuthConstants.TOKEN_URL); mockRequest.setParameter(OAuthConstants.GRANT_TYPE, OAuthConstants.AUTHORIZATION_CODE); mockRequest.setParameter(OAuthConstants.CODE, CODE); mockRequest.setParameter(OAuthConstants.CLIENT_ID, CLIENT_ID); @@ -522,8 +554,8 @@ public void verifyRegexRedirectUriDoesNotStartWithServiceId() throws Exception { final ObjectMapper mapper = new ObjectMapper(); - final String expected = "{\"error\":\"" + OAuthConstants.INVALID_REQUEST + "\",\"error_description\":\"" - + OAuthConstants.INVALID_REDIRECT_URI_DESCRIPTION + "\"}"; + final String expected = "{\"error\":\"" + OAuthConstants.INVALID_REQUEST + + "\",\"error_description\":\"" + OAuthConstants.INVALID_REDIRECT_URI_DESCRIPTION + "\"}"; final JsonNode expectedObj = mapper.readTree(expected); final JsonNode receivedObj = mapper.readTree(mockResponse.getContentAsString()); assertEquals(expectedObj.get("error").asText(), receivedObj.get("error").asText()); @@ -531,12 +563,13 @@ public void verifyRegexRedirectUriDoesNotStartWithServiceId() throws Exception { } /** - * Verify that regex-like redirect URI matches if an only if the two string are equal (case-insensitive). + * Verify that regex-like redirect URI matches if and only if the two strings are equal (case-insensitive). * - * Online Mode + * ONLINE mode. */ @Test public void verifyOnlineOKRegexRedirectUri() throws Exception { + final ServiceTicket serviceTicket = mock(ServiceTicket.class); when(serviceTicket.getCreationTime()).thenReturn(new Date().getTime()); @@ -555,8 +588,8 @@ public void verifyOnlineOKRegexRedirectUri() throws Exception { when(centralOAuthService.getRegisteredService(CLIENT_ID)).thenReturn(service); when(centralOAuthService.grantOnlineAccessToken(authorizationCode)).thenReturn(accessToken); - final MockHttpServletRequest mockRequest = new MockHttpServletRequest("POST", CONTEXT - + OAuthConstants.TOKEN_URL); + final MockHttpServletRequest mockRequest + = new MockHttpServletRequest("POST", CONTEXT + OAuthConstants.TOKEN_URL); mockRequest.setParameter(OAuthConstants.GRANT_TYPE, OAuthConstants.AUTHORIZATION_CODE); mockRequest.setParameter(OAuthConstants.CODE, CODE); mockRequest.setParameter(OAuthConstants.CLIENT_ID, CLIENT_ID); @@ -576,23 +609,29 @@ public void verifyOnlineOKRegexRedirectUri() throws Exception { assertEquals("application/json", mockResponse.getContentType()); final ObjectMapper mapper = new ObjectMapper(); - final String expected = "{\"token_type\":\"" + OAuthConstants.BEARER_TOKEN + "\",\"expires_in\":\"" + TIMEOUT - + "\",\"access_token\":\"" + AT_ID + "\"}"; + final String expected = "{" + + "\"token_type\":\"" + OAuthConstants.BEARER_TOKEN + "\"," + + "\"expires_in\":\"" + TIMEOUT + "\"," + + "\"access_token\":\"" + AT_ID + "\"" + + "}"; final JsonNode expectedObj = mapper.readTree(expected); final JsonNode receivedObj = mapper.readTree(mockResponse.getContentAsString()); assertEquals(expectedObj.get("token_type").asText(), receivedObj.get("token_type").asText()); - assertTrue("received expires_at greater or equal to expected", - expectedObj.get("expires_in").asInt() >= receivedObj.get("expires_in").asInt()); + assertTrue( + "received expires_at greater or equal to expected", + expectedObj.get("expires_in").asInt() >= receivedObj.get("expires_in").asInt() + ); assertEquals(expectedObj.get("access_token").asText(), receivedObj.get("access_token").asText()); } /** - * Verify that regex-like redirect URI matches if an only if the two string are equal (case-insensitive). + * Verify that regex-like redirect URI matches if and only if the two strings are equal (case-insensitive). * * Offline Mode */ @Test public void verifyOfflineOKRegexRedirectUri() throws Exception { + final ServiceTicket serviceTicket = mock(ServiceTicket.class); when(serviceTicket.getCreationTime()).thenReturn(new Date().getTime()); @@ -615,8 +654,8 @@ public void verifyOfflineOKRegexRedirectUri() throws Exception { when(centralOAuthService.grantOfflineRefreshToken(authorizationCode, REGEX_REDIRECT_URI)).thenReturn(refreshToken); when(centralOAuthService.grantOfflineAccessToken(refreshToken)).thenReturn(accessToken); - final MockHttpServletRequest mockRequest = new MockHttpServletRequest("POST", CONTEXT - + OAuthConstants.TOKEN_URL); + final MockHttpServletRequest mockRequest + = new MockHttpServletRequest("POST", CONTEXT + OAuthConstants.TOKEN_URL); mockRequest.setParameter(OAuthConstants.GRANT_TYPE, OAuthConstants.AUTHORIZATION_CODE); mockRequest.setParameter(OAuthConstants.CODE, CODE); mockRequest.setParameter(OAuthConstants.CLIENT_ID, CLIENT_ID); @@ -636,18 +675,25 @@ public void verifyOfflineOKRegexRedirectUri() throws Exception { assertEquals("application/json", mockResponse.getContentType()); final ObjectMapper mapper = new ObjectMapper(); - final String expected = "{\"token_type\":\"" + OAuthConstants.BEARER_TOKEN + "\",\"expires_in\":\"" + TIMEOUT - + "\",\"refresh_token\":\"" + RT_ID + "\",\"access_token\":\"" + AT_ID + "\"}"; + final String expected = "{" + + "\"token_type\":\"" + OAuthConstants.BEARER_TOKEN + "\"," + + "\"expires_in\":\"" + TIMEOUT + "\"," + +"\"refresh_token\":\"" + RT_ID + "\"," + + "\"access_token\":\"" + AT_ID + "\"" + + "}"; final JsonNode expectedObj = mapper.readTree(expected); final JsonNode receivedObj = mapper.readTree(mockResponse.getContentAsString()); assertEquals(expectedObj.get("token_type").asText(), receivedObj.get("token_type").asText()); - assertTrue("received expires_at greater or equal to expected", - expectedObj.get("expires_in").asInt() >= receivedObj.get("expires_in").asInt()); + assertTrue( + "received expires_at greater or equal to expected", + expectedObj.get("expires_in").asInt() >= receivedObj.get("expires_in").asInt() + ); assertEquals(expectedObj.get("refresh_token").asText(), receivedObj.get("refresh_token").asText()); assertEquals(expectedObj.get("access_token").asText(), receivedObj.get("access_token").asText()); } private OAuthRegisteredService getRegisteredService(final String serviceId, final String secret) { + final OAuthRegisteredService registeredServiceImpl = new OAuthRegisteredService(); registeredServiceImpl.setName("The registered service name"); diff --git a/cas-server-support-oauth/src/test/java/org/jasig/cas/support/oauth/web/OAuth20TokenRefreshTokenControllerTests.java b/cas-server-support-oauth/src/test/java/org/jasig/cas/support/oauth/web/OAuth20TokenRefreshTokenControllerTests.java index 5c3a0fa5..9ab2de7c 100644 --- a/cas-server-support-oauth/src/test/java/org/jasig/cas/support/oauth/web/OAuth20TokenRefreshTokenControllerTests.java +++ b/cas-server-support-oauth/src/test/java/org/jasig/cas/support/oauth/web/OAuth20TokenRefreshTokenControllerTests.java @@ -18,13 +18,11 @@ */ package org.jasig.cas.support.oauth.web; -import static org.junit.Assert.*; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; + import org.apache.http.HttpStatus; + import org.jasig.cas.support.oauth.CentralOAuthService; import org.jasig.cas.support.oauth.InvalidParameterException; import org.jasig.cas.support.oauth.OAuthConstants; @@ -33,7 +31,15 @@ import org.jasig.cas.support.oauth.token.InvalidTokenException; import org.jasig.cas.support.oauth.token.RefreshToken; import org.jasig.cas.ticket.TicketGrantingTicket; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; import org.junit.Test; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.web.servlet.ModelAndView; @@ -45,7 +51,9 @@ * * @author Jerome Leleu * @author Michael Haselton - * @since 3.5.2 + * @author Fitz Elliott + * @author Longze Chen + * @since 4.1.5 */ public final class OAuth20TokenRefreshTokenControllerTests { @@ -71,8 +79,9 @@ public final class OAuth20TokenRefreshTokenControllerTests { @Test public void verifyNoRefreshTokenID() throws Exception { - final MockHttpServletRequest mockRequest = new MockHttpServletRequest("POST", CONTEXT - + OAuthConstants.TOKEN_URL); + + final MockHttpServletRequest mockRequest + = new MockHttpServletRequest("POST", CONTEXT + OAuthConstants.TOKEN_URL); mockRequest.setParameter(OAuthConstants.GRANT_TYPE, OAuthConstants.REFRESH_TOKEN); mockRequest.setParameter(OAuthConstants.CLIENT_ID, CLIENT_ID); mockRequest.setParameter(OAuthConstants.CLIENT_SECRET, CLIENT_SECRET); @@ -99,8 +108,9 @@ public void verifyNoRefreshTokenID() throws Exception { @Test public void verifyNoClientId() throws Exception { - final MockHttpServletRequest mockRequest = new MockHttpServletRequest("POST", CONTEXT - + OAuthConstants.TOKEN_URL); + + final MockHttpServletRequest mockRequest + = new MockHttpServletRequest("POST", CONTEXT + OAuthConstants.TOKEN_URL); mockRequest.setParameter(OAuthConstants.GRANT_TYPE, OAuthConstants.REFRESH_TOKEN); mockRequest.setParameter(OAuthConstants.REFRESH_TOKEN, RT_ID); mockRequest.setParameter(OAuthConstants.CLIENT_SECRET, CLIENT_SECRET); @@ -127,8 +137,9 @@ public void verifyNoClientId() throws Exception { @Test public void verifyNoClientSecret() throws Exception { - final MockHttpServletRequest mockRequest = new MockHttpServletRequest("POST", CONTEXT - + OAuthConstants.TOKEN_URL); + + final MockHttpServletRequest mockRequest + = new MockHttpServletRequest("POST", CONTEXT + OAuthConstants.TOKEN_URL); mockRequest.setParameter(OAuthConstants.GRANT_TYPE, OAuthConstants.REFRESH_TOKEN); mockRequest.setParameter(OAuthConstants.REFRESH_TOKEN, RT_ID); mockRequest.setParameter(OAuthConstants.CLIENT_ID, CLIENT_ID); @@ -155,11 +166,12 @@ public void verifyNoClientSecret() throws Exception { @Test public void verifyNoRefreshToken() throws Exception { + final CentralOAuthService centralOAuthService = mock(CentralOAuthService.class); when(centralOAuthService.getToken(RT_ID, RefreshToken.class)).thenThrow(new InvalidTokenException("error")); - final MockHttpServletRequest mockRequest = new MockHttpServletRequest("POST", CONTEXT - + OAuthConstants.TOKEN_URL); + final MockHttpServletRequest mockRequest + = new MockHttpServletRequest("POST", CONTEXT + OAuthConstants.TOKEN_URL); mockRequest.setParameter(OAuthConstants.GRANT_TYPE, OAuthConstants.REFRESH_TOKEN); mockRequest.setParameter(OAuthConstants.REFRESH_TOKEN, RT_ID); mockRequest.setParameter(OAuthConstants.CLIENT_ID, CLIENT_ID); @@ -178,8 +190,8 @@ public void verifyNoRefreshToken() throws Exception { final ObjectMapper mapper = new ObjectMapper(); - final String expected = "{\"error\":\"" + OAuthConstants.INVALID_REQUEST + "\",\"error_description\":\"" - + OAuthConstants.INVALID_REFRESH_TOKEN_DESCRIPTION + "\"}"; + final String expected = "{\"error\":\"" + OAuthConstants.INVALID_REQUEST + + "\",\"error_description\":\"" + OAuthConstants.INVALID_REFRESH_TOKEN_DESCRIPTION + "\"}"; final JsonNode expectedObj = mapper.readTree(expected); final JsonNode receivedObj = mapper.readTree(mockResponse.getContentAsString()); assertEquals(expectedObj.get("error").asText(), receivedObj.get("error").asText()); @@ -188,6 +200,7 @@ public void verifyNoRefreshToken() throws Exception { @Test public void verifyOK() throws Exception { + final TicketGrantingTicket ticketGrantingTicket = mock(TicketGrantingTicket.class); when(ticketGrantingTicket.getCreationTime()).thenReturn(new Date().getTime()); @@ -205,8 +218,8 @@ public void verifyOK() throws Exception { when(centralOAuthService.getRegisteredService(CLIENT_ID)).thenReturn(service); when(centralOAuthService.grantOfflineAccessToken(refreshToken)).thenReturn(accessToken); - final MockHttpServletRequest mockRequest = new MockHttpServletRequest("POST", CONTEXT - + OAuthConstants.TOKEN_URL); + final MockHttpServletRequest mockRequest + = new MockHttpServletRequest("POST", CONTEXT + OAuthConstants.TOKEN_URL); mockRequest.setParameter(OAuthConstants.GRANT_TYPE, OAuthConstants.REFRESH_TOKEN); mockRequest.setParameter(OAuthConstants.REFRESH_TOKEN, RT_ID); mockRequest.setParameter(OAuthConstants.CLIENT_ID, CLIENT_ID); @@ -225,17 +238,25 @@ public void verifyOK() throws Exception { assertEquals("application/json", mockResponse.getContentType()); final ObjectMapper mapper = new ObjectMapper(); - final String expected = "{\"token_type\":\"" + OAuthConstants.BEARER_TOKEN + "\",\"expires_in\":\"" + TIMEOUT - + "\",\"refresh_token\":\"" + RT_ID + "\",\"access_token\":\"" + AT_ID + "\"}"; + final String expected = "{" + + "\"token_type\":\"" + OAuthConstants.BEARER_TOKEN + "\"," + + "\"expires_in\":\"" + TIMEOUT + "\"," + + "\"refresh_token\":\"" + RT_ID + "\"," + + "\"access_token\":\"" + AT_ID + "\"" + + "}"; + final JsonNode expectedObj = mapper.readTree(expected); final JsonNode receivedObj = mapper.readTree(mockResponse.getContentAsString()); assertEquals(expectedObj.get("token_type").asText(), receivedObj.get("token_type").asText()); - assertTrue("received expires_at greater or equal to expected", - expectedObj.get("expires_in").asInt() >= receivedObj.get("expires_in").asInt()); + assertTrue( + "received expires_at greater or equal to expected", + expectedObj.get("expires_in").asInt() >= receivedObj.get("expires_in").asInt() + ); assertEquals(expectedObj.get("access_token").asText(), receivedObj.get("access_token").asText()); } private OAuthRegisteredService getRegisteredService(final String serviceId, final String secret) { + final OAuthRegisteredService registeredServiceImpl = new OAuthRegisteredService(); registeredServiceImpl.setName("The registered service name"); diff --git a/cas-server-support-oauth/src/test/java/org/jasig/cas/support/oauth/web/OAuth20WrapperControllerTests.java b/cas-server-support-oauth/src/test/java/org/jasig/cas/support/oauth/web/OAuth20WrapperControllerTests.java index 35e6c935..344fcf04 100644 --- a/cas-server-support-oauth/src/test/java/org/jasig/cas/support/oauth/web/OAuth20WrapperControllerTests.java +++ b/cas-server-support-oauth/src/test/java/org/jasig/cas/support/oauth/web/OAuth20WrapperControllerTests.java @@ -22,10 +22,14 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; + import org.apache.http.HttpStatus; + import org.jasig.cas.support.oauth.InvalidParameterException; import org.jasig.cas.support.oauth.OAuthConstants; + import org.junit.Test; + import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; @@ -33,7 +37,9 @@ * This class tests the {@link OAuth20WrapperController} class. * * @author Jerome Leleu - * @since 3.5.2 + * @author Fitz Elliott + * @author Longze Chen + * @since 4.1.5 */ public final class OAuth20WrapperControllerTests { @@ -41,7 +47,9 @@ public final class OAuth20WrapperControllerTests { @Test public void verifyWrongMethod() throws Exception { - final MockHttpServletRequest mockRequest = new MockHttpServletRequest("GET", CONTEXT + "wrongmethod"); + + final MockHttpServletRequest mockRequest + = new MockHttpServletRequest("GET", CONTEXT + "wrongmethod"); final MockHttpServletResponse mockResponse = new MockHttpServletResponse(); @@ -55,7 +63,9 @@ public void verifyWrongMethod() throws Exception { @Test public void verifyNoPostForAuthCtrl() throws Exception { - final MockHttpServletRequest mockRequest = new MockHttpServletRequest("POST", CONTEXT + OAuthConstants.AUTHORIZE_URL); + + final MockHttpServletRequest mockRequest + = new MockHttpServletRequest("POST", CONTEXT + OAuthConstants.AUTHORIZE_URL); final MockHttpServletResponse mockResponse = new MockHttpServletResponse(); @@ -69,7 +79,9 @@ public void verifyNoPostForAuthCtrl() throws Exception { @Test public void verifyNoPostForAuthCallbackCtrl() throws Exception { - final MockHttpServletRequest mockRequest = new MockHttpServletRequest("POST", CONTEXT + OAuthConstants.CALLBACK_AUTHORIZE_URL); + + final MockHttpServletRequest mockRequest + = new MockHttpServletRequest("POST", CONTEXT + OAuthConstants.CALLBACK_AUTHORIZE_URL); final MockHttpServletResponse mockResponse = new MockHttpServletResponse(); @@ -83,8 +95,9 @@ public void verifyNoPostForAuthCallbackCtrl() throws Exception { @Test public void verifyNoPostForAuthCallbackActionCtrl() throws Exception { - final MockHttpServletRequest mockRequest = new MockHttpServletRequest( - "POST", CONTEXT + OAuthConstants.CALLBACK_AUTHORIZE_ACTION_URL); + + final MockHttpServletRequest mockRequest + = new MockHttpServletRequest("POST", CONTEXT + OAuthConstants.CALLBACK_AUTHORIZE_ACTION_URL); final MockHttpServletResponse mockResponse = new MockHttpServletResponse(); @@ -97,9 +110,10 @@ public void verifyNoPostForAuthCallbackActionCtrl() throws Exception { } @Test - public void verifyNoGetForTokenCtrls() throws Exception { - final MockHttpServletRequest mockRequest = new MockHttpServletRequest( - "GET", CONTEXT + OAuthConstants.TOKEN_URL); + public void verifyNoGetForTokenCtrl() throws Exception { + + final MockHttpServletRequest mockRequest + = new MockHttpServletRequest("GET", CONTEXT + OAuthConstants.TOKEN_URL); final MockHttpServletResponse mockResponse = new MockHttpServletResponse(); @@ -112,9 +126,10 @@ public void verifyNoGetForTokenCtrls() throws Exception { } @Test - public void verifyNoGrantTypeForTokenCtrls() throws Exception { - final MockHttpServletRequest mockRequest = new MockHttpServletRequest( - "POST", CONTEXT + OAuthConstants.TOKEN_URL); + public void verifyNoGrantTypeForTokenCtrl() throws Exception { + + final MockHttpServletRequest mockRequest + = new MockHttpServletRequest("POST", CONTEXT + OAuthConstants.TOKEN_URL); final MockHttpServletResponse mockResponse = new MockHttpServletResponse(); @@ -126,8 +141,8 @@ public void verifyNoGrantTypeForTokenCtrls() throws Exception { final ObjectMapper mapper = new ObjectMapper(); - final String expected = "{\"error\":\"" + OAuthConstants.INVALID_REQUEST + "\",\"error_description\":\"" - + new InvalidParameterException(OAuthConstants.GRANT_TYPE).getMessage() + "\"}"; + final String expected = "{\"error\":\"" + OAuthConstants.INVALID_REQUEST + + "\",\"error_description\":\"" + new InvalidParameterException(OAuthConstants.GRANT_TYPE).getMessage() + "\"}"; final JsonNode expectedObj = mapper.readTree(expected); final JsonNode receivedObj = mapper.readTree(mockResponse.getContentAsString()); assertEquals(expectedObj.get("error").asText(), receivedObj.get("error").asText()); @@ -135,9 +150,10 @@ public void verifyNoGrantTypeForTokenCtrls() throws Exception { } @Test - public void verifyInvalidGrantTypeForTokenCtrls() throws Exception { - final MockHttpServletRequest mockRequest = new MockHttpServletRequest( - "POST", CONTEXT + OAuthConstants.TOKEN_URL); + public void verifyInvalidGrantTypeForTokenCtrl() throws Exception { + + final MockHttpServletRequest mockRequest + = new MockHttpServletRequest("POST", CONTEXT + OAuthConstants.TOKEN_URL); final MockHttpServletResponse mockResponse = new MockHttpServletResponse(); mockRequest.setParameter(OAuthConstants.GRANT_TYPE, "banana"); @@ -150,8 +166,8 @@ public void verifyInvalidGrantTypeForTokenCtrls() throws Exception { final ObjectMapper mapper = new ObjectMapper(); - final String expected = "{\"error\":\"" + OAuthConstants.INVALID_REQUEST + "\",\"error_description\":\"" - + new InvalidParameterException(OAuthConstants.GRANT_TYPE).getMessage() + "\"}"; + final String expected = "{\"error\":\"" + OAuthConstants.INVALID_REQUEST + + "\",\"error_description\":\"" + new InvalidParameterException(OAuthConstants.GRANT_TYPE).getMessage() + "\"}"; final JsonNode expectedObj = mapper.readTree(expected); final JsonNode receivedObj = mapper.readTree(mockResponse.getContentAsString()); assertEquals(expectedObj.get("error").asText(), receivedObj.get("error").asText()); @@ -159,9 +175,10 @@ public void verifyInvalidGrantTypeForTokenCtrls() throws Exception { } @Test - public void verifyNoGetForRevokeCtrls() throws Exception { - final MockHttpServletRequest mockRequest = new MockHttpServletRequest( - "GET", CONTEXT + OAuthConstants.REVOKE_URL); + public void verifyNoGetForRevokeCtrl() throws Exception { + + final MockHttpServletRequest mockRequest + = new MockHttpServletRequest("GET", CONTEXT + OAuthConstants.REVOKE_URL); final MockHttpServletResponse mockResponse = new MockHttpServletResponse(); @@ -175,8 +192,9 @@ public void verifyNoGetForRevokeCtrls() throws Exception { @Test public void verifyNoPostForProfileCtrl() throws Exception { - final MockHttpServletRequest mockRequest = new MockHttpServletRequest( - "POST", CONTEXT + OAuthConstants.PROFILE_URL); + + final MockHttpServletRequest mockRequest + = new MockHttpServletRequest("POST", CONTEXT + OAuthConstants.PROFILE_URL); final MockHttpServletResponse mockResponse = new MockHttpServletResponse(); @@ -190,8 +208,9 @@ public void verifyNoPostForProfileCtrl() throws Exception { @Test public void verifyNoGetForProfileCtrl() throws Exception { - final MockHttpServletRequest mockRequest = new MockHttpServletRequest( - "GET", CONTEXT + OAuthConstants.METADATA_URL); + + final MockHttpServletRequest mockRequest + = new MockHttpServletRequest("GET", CONTEXT + OAuthConstants.METADATA_URL); final MockHttpServletResponse mockResponse = new MockHttpServletResponse(); @@ -202,5 +221,4 @@ public void verifyNoGetForProfileCtrl() throws Exception { assertEquals("text/plain", mockResponse.getContentType()); assertEquals("error=" + OAuthConstants.INVALID_REQUEST, mockResponse.getContentAsString()); } - } diff --git a/cas-server-support-osf/src/main/java/io/cos/cas/adaptors/postgres/models/OpenScienceFrameworkTimeBasedOneTimePassword.java b/cas-server-support-osf/src/main/java/io/cos/cas/adaptors/postgres/models/OpenScienceFrameworkTimeBasedOneTimePassword.java index f5427551..d9c3aa9d 100644 --- a/cas-server-support-osf/src/main/java/io/cos/cas/adaptors/postgres/models/OpenScienceFrameworkTimeBasedOneTimePassword.java +++ b/cas-server-support-osf/src/main/java/io/cos/cas/adaptors/postgres/models/OpenScienceFrameworkTimeBasedOneTimePassword.java @@ -54,7 +54,7 @@ public class OpenScienceFrameworkTimeBasedOneTimePassword { @Column(name = "is_confirmed", nullable = false) private Boolean confirmed; - @Column(name = "deleted", nullable = false) + @Column(name = "is_deleted", nullable = false) private Boolean deleted; /** Default Constructor. */ diff --git a/checkstyle-rules.xml b/checkstyle-rules.xml index ca2f1828..5c79da83 100644 --- a/checkstyle-rules.xml +++ b/checkstyle-rules.xml @@ -225,7 +225,7 @@ Checkstyle configuration based on Sun's conventions, compliant with CAS coding c - + diff --git a/docs/osf-cas-as-a-cas-client.md b/docs/osf-cas-as-a-cas-client.md new file mode 100644 index 00000000..54041c47 --- /dev/null +++ b/docs/osf-cas-as-a-cas-client.md @@ -0,0 +1,5 @@ +# OSF CAS as a CAS Client + +[Apereo CAS: CAS Protocol](https://apereo.github.io/cas/4.1.x/protocol/CAS-Protocol.html) + +[Apereo CAS: Delegate Authentication](https://apereo.github.io/cas/4.1.x/integration/Delegate-Authentication.html) diff --git a/docs/osf-cas-as-a-saml-sp.md b/docs/osf-cas-as-a-saml-sp.md new file mode 100644 index 00000000..cb1f9c26 --- /dev/null +++ b/docs/osf-cas-as-a-saml-sp.md @@ -0,0 +1,3 @@ +# OSF CAS as a SAML 2.0 Service Provider (SP) + +This feature requires a [Shibboleth 2.x](https://wiki.shibboleth.net/confluence/display/SHIB2/Home) server sitting in front and serving as a SAML 2.0 Service Provider (SP). diff --git a/docs/osf-cas-as-an-oauth-client.md b/docs/osf-cas-as-an-oauth-client.md new file mode 100644 index 00000000..a666d2f2 --- /dev/null +++ b/docs/osf-cas-as-an-oauth-client.md @@ -0,0 +1,5 @@ +# OSF CAS as an OAuth Client + +[Apereo CAS: OAuth Protocol](https://apereo.github.io/cas/4.1.x/protocol/OAuth-Protocol.html) + +[Apereo CAS: Delegate Authentication](https://apereo.github.io/cas/4.1.x/integration/Delegate-Authentication.html) diff --git a/docs/osf-cas-as-an-oauth-server.md b/docs/osf-cas-as-an-oauth-server.md new file mode 100644 index 00000000..fc1d1922 --- /dev/null +++ b/docs/osf-cas-as-an-oauth-server.md @@ -0,0 +1,413 @@ +# OSF CAS as an OAuth Server + +## About + +OSF CAS serves as an OAuth 2.0 authorization server for OSF in addition to its primary role as a CAS authentication server. + +### The OAuth 2.0 Protocol + +* [RFC 6749](https://tools.ietf.org/html/rfc6749) +* [OAuth 2.0](https://oauth.net/2/) +* [OAuth 2.0 Simplified](https://aaronparecki.com/oauth-2-simplified/) + +### Parties and Roles + +Party | Who | Role +----------------------- | ----------------- | ---- +Client Application | A web application | +Authorization Server | OSF CAS | +Resource Owner | OSF users | +Resource Server | OSF API | + +### Enable OAuth 2.0 Server Support + +* [Apereo CAS: OAuth Protocol](https://apereo.github.io/cas/4.1.x/protocol/OAuth-Protocol.html) +* [Apereo CAS: CAS as OAuth Server](https://apereo.github.io/cas/4.1.x/installation/OAuth-OpenId-Authentication.html) + +
+ +## Features + +### General + +* Authorize client applications +* Exchange authorization code for access and refresh token +* Request access token using refresh token +* Revoke access and refresh tokens + +### Client Application Owners + +* Get the number of users who have authorized the application +* Revoke all tokens of the application for all users + +### Resource Owners + +* List all authorized applications +* Revoke all tokens of an authorized application for the owner + +
+ +## Design and Implementation + +For implementation details, please refer to the [`cas-server-support-oauth`](https://github.com/CenterForOpenScience/cas-overlay/tree/develop/cas-server-support-oauth/src/main/java/org/jasig/cas) module. + +### Tokens and Token Management + +* Token + * [Authorization Code](https://github.com/CenterForOpenScience/cas-overlay/blob/develop/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/token/AuthorizationCodeImpl.java) (AC) + * [Access Token](https://github.com/CenterForOpenScience/cas-overlay/blob/develop/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/token/AccessTokenImpl.java) (AT) + * [Refresh Token](https://github.com/CenterForOpenScience/cas-overlay/blob/develop/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/token/RefreshTokenImpl.java) (RT) +* [Token Type](https://github.com/CenterForOpenScience/cas-overlay/blob/develop/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/token/TokenType.java) + * `OFFLINE`: AC, AC-exchanged RT and RT-granted AT + * `ONLINE`: AC, AC-exchanged AT + * `PERSONAL`: AT granted based on existing [OSF Personal Access Token](https://github.com/CenterForOpenScience/cas-overlay/blob/develop/cas-server-support-osf/src/main/java/io/cos/cas/adaptors/postgres/models/OpenScienceFrameworkApiOauth2PersonalAccessToken.java) (OSF PAT) + * `CAS`: AT granted as attributes during service validation +* Token Access and Storage + * [JPA Token Registry](https://github.com/CenterForOpenScience/cas-overlay/blob/develop/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/token/registry/JpaTokenRegistry.java) + +### Scope and Scope Management + +In OSF CAS, [Scope](https://github.com/CenterForOpenScience/cas-overlay/blob/develop/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/scope/Scope.java) is not stored by itself in the CAS DB but as an [LOB property](https://github.com/CenterForOpenScience/cas-overlay/blob/develop/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/token/AbstractToken.java#L65) of the associated [Token](https://github.com/CenterForOpenScience/cas-overlay/blob/develop/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/token/AbstractToken.java). The [Scope Manager](https://github.com/CenterForOpenScience/cas-overlay/blob/develop/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/scope/ScopeManager.java) uses 1) [Simple OAuth Scope Handler](https://github.com/CenterForOpenScience/cas-overlay/blob/develop/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/scope/handler/SimpleScopeHandler.java) to handle scopes associated with `CAS` ATs only and 2) the [OSF Scope Handler](https://github.com/CenterForOpenScience/cas-overlay/blob/develop/cas-server-support-osf/src/main/java/io/cos/cas/adaptors/postgres/handlers/OpenScienceFrameworkScopeHandler.java) to handle scopes for all other types that use [OSF Scope]([OSF Scope](https://github.com/CenterForOpenScience/cas-overlay/blob/develop/cas-server-support-osf/src/main/java/io/cos/cas/adaptors/postgres/models/OpenScienceFrameworkApiOauth2Scope.java). + +### Service and Service Management + +In OSF CAS, client applications are loaded (and periodically updated) from the [OSF DB](https://github.com/CenterForOpenScience/cas-overlay/blob/develop/cas-server-support-osf/src/main/java/io/cos/cas/adaptors/postgres/models/OpenScienceFrameworkApiOauth2Application.java) into the memory as [OAuth Registered Service](https://github.com/CenterForOpenScience/cas-overlay/blob/develop/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/services/OAuthRegisteredService.java). The main [OAuth Service](https://github.com/CenterForOpenScience/cas-overlay/blob/develop/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/CentralOAuthServiceImpl.java) accesses them via the [Services Manager](https://github.com/CenterForOpenScience/cas-overlay/blob/develop/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/CentralOAuthServiceImpl.java#L154). + +### Delegated Ticket Expiration + +The expiration time for a token is determined by [the expiration policy](https://github.com/CenterForOpenScience/cas-overlay/blob/develop/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/ticket/support/OAuthDelegatingExpirationPolicy.java) its ticket uses. + +* `ONLINE` / `OFFLINE` AC: 1 minute +* `ONLINE` / `OFFLINE` AT: 1 hour +* `OFFLINE` RT: never-expire +* `PERSONAL` AT: never-expire +* `CAS` AT: varies - 30-day if *Remember Me* is enabled; otherwise 8-hour maximum lifetime with a 2-hour sliding window. + +### CAS Service Validation + +The [OAuth 2.0 Service Validate Controller](https://github.com/CenterForOpenScience/cas-overlay/blob/develop/cas-server-support-oauth/src/main/java/org/jasig/cas/support/oauth/web/OAuth20ServiceValidateController.java) replaces the default [CAS Service Validate Controller](https://github.com/apereo/cas/blob/4.1.x/cas-server-webapp-support/src/main/java/org/jasig/cas/web/ServiceValidateController.java). In addition to performing default duties, it injects a `CAS` AT into the attributes released to OSF users during primary authentication. + +
+ +## Endpoints + +### `GET /oauth2/authorize` + +#### Authorize a Client Application + +Securely allows or denies the client application's request to access the resource user's information with specified scopes. It returns a one-time and short-lived authorization code, which will be used to follow up with the token-code exchange. + +##### Request + +``` +https://accounts.osf.io/oauth2/authorize +``` + +##### Request Query Parameters + +Parameter | Value / Example | Description +--------------- | --------------------------------- | ----------- +response_type | code | +client_id | ffe5247b810045a8a9277d3b3b4edc7a | +redirect_uri | https://my.app.io/oauth2/callback | +scope | osf.full_read | +state | OQlHiyBY | +access_type | **online** / offline | +approval_prompt | **auto** / force` | + +##### Authorization Web Flow + +0. The client application issues the initial authorization request to `oauth2/authorize`. +1. If the user is not logged in, redirects to the primary CAS login with `oauth2/callbackAuthorize` as service. +2. Both step 0 and 1 end up redirecting the user to `oauth2/callbackAuthorize` for service validation. +3. After validation, redirects to `oauth2/callbackAuthorize` one more time, which checks previous decisions and asks the user to allow or deny the authorization if necessary. +4. If denied, redirects to `/oauth2/callbackAuthorizeAction?action=DENY`; if allowed, redirects to `/oauth2/callbackAuthorizeAction?action=ALLOW` +5. Finally for both decisions in step 4, redirects the user to the *Redirect URI*: `https://my.app.io/oauth2/callback?` with different query parameters as shown below. + +##### Query Parameters: ALLOW + +Parameter | Value / Example +----------- | ------------------------------------------------------- +code | AC-1-mFs7MrWvaQy1fiidWGwXTw4dbAH30wk39cAELJnxizjGCUXYJl +state | OQlHiyBY + +##### Query Parameters: DENY + +Parameter | Value / Example +----------- | --------------- +error | access_denied + +
+ +### `POST /oauth2/token` + +#### Exchange Code for Token + +Exchanges the authorization code for an access token and potentially a refresh token if *offline* mode was specified. + +##### Request + +``` +https://accounts.osf.io/oauth2/token +``` + +##### `POST` Body Parameters + +Parameter | Value / Example | Description +--------------- | --------------------------------------------------------- | ----------- +code | AC-1-mFs7MrWvaQy1fiidWGwXTw4dbAH30wk39cAELJnxizjGCUXYJl | +client_id | ffe5247b810045a8a9277d3b3b4edc7a | +client_secret | 5PgE96R3Z53dBuwBDkJfbK6ItDXvGhaxYpQ6r4cU | +redirect_uri | https://my.app.io/oauth2/callback | +grant_type | authorization_code | + +##### Response + +``` +HTTP 200 OK +``` + +###### ONLINE Mode + +```json +{ + "access_token": "AT-2-p5jtVLATgft5EHqqbCTagg5i3q9e1htdcGEBvcpq0l1b2RyQav4bItEKPcDh94c5z7d7EK", + "token_type": "Bearer", + "expires_in": 3600 +} +``` + +###### OFFLINE Mode + +```json +{ + "access_token": "AT-1-IBGuzWBdencAMz74LQkIuNcbLuu9WM3TYyacadkecrHUlcivs1GnWHjFmlkZPYg4TTAUM4", + "refresh_token": "RT-1-xfQXZaqXSQIJykCg2vnfdQjc5efVKdtteXaPo0OwCxWzIAacfC", + "token_type": "Bearer", + "expires_in": 3600 +} +``` + +#### Refresh Access Token + +In *offline* mode, the client application may request for a new access token by presenting the previously granted refresh token. + +##### Request + +``` +https://accounts.osf.io/oauth2/token +``` + +##### `POST` Body Parameters + +Parameter | Value / Example | Description +--------------- | --------------------------------------------------------- | ----------- +refresh_token | RT-1-xfQXZaqXSQIJykCg2vnfdQjc5efVKdtteXaPo0OwCxWzIAacfC | +client_id | ffe5247b810045a8a9277d3b3b4edc7a | +client_secret | 5PgE96R3Z53dBuwBDkJfbK6ItDXvGhaxYpQ6r4cU | +grant_type | refresh_token | + +##### Response + +``` +HTTP 200 OK +``` + +```json +{ + "access_token": "AT-3-WbBmXVTsPlhUatrs5sQmilVLnA30wVv3holmfFCbIfePRjzQ6UXCb7LwJHGbFqmad3wNXu", + "token_type": "Bearer", + "expires_in": 3600 +} +``` + +
+ +### `GET /oauth2/profile` + +#### Profile + +Provides the user's principal ID, any released attributes and a list of granted scopes. + +##### Request + +``` +https://accounts.osf.io/oauth2/profile +``` + +##### Authorization Header + +Name | Value / Example +--------------- | ---------------------------------------------------------------------------------- +Authorization | Bearer AT-4-IdanI4hWiybRzARBiLrlMdeMTlDJIqo1UgVLb4MHzbF13pNIT5POrfQTMW5yEyVD1oXXcz + +##### Response + +``` +HTTP 200 OK +``` + +```json +{ + "scope": [ + "osf.full_read" + ], + "id": "f2t7d" +} +``` + +
+ +### `POST /oauth2/metadata` + +#### Metadata about a Client Application + +Provides metadata about an application specified by the given client ID. + +##### Request + +``` +https://accounts.osf.io/oauth2/metadata +``` + +##### `POST` Body Parameters + +Parameter | Value / Example | Description +--------------- | ----------------------------------------- | ----------- +client_id | ffe5247b810045a8a9277d3b3b4edc7a | +client_secret | 5PgE96R3Z53dBuwBDkJfbK6ItDXvGhaxYpQ6r4cU | + +##### Response + +``` +HTTP 204 NO CONTENT +``` + +```json +{ + "name": "An OAuth 2.0 Developer App", + "description": "See https://my.app.io/about.", + "client_id": "ffe5247b810045a8a9277d3b3b4edc7a", + "users": 1023 +} +``` + +#### Metadata about a Resource User + +Gathers metadata regarding a user specified by the principal ID associated with with the access token, *which must be of token type `CAS`*. + +##### Request + +``` +https://accounts.osf.io/oauth2/metadata +``` + +##### `POST` Body Parameters + +Name | Value / Example +--------------- | ---------------------------------------------------------------------------------- +Authorization | Bearer AT-5-OQlHiyBYZwnwqI9Qu6o6Z1fZl7rbx6TzTZB9yPay6SOcbXwfdvpjc6FTbBpgwrj6PMF9GX + +##### Response + +``` +HTTP 200 OK +``` + +```json +[ + { + "name": "An OAuth 2.0 Developer App", + "description": "See https://my.app.io/about.", + "client_id": "ffe5247b810045a8a9277d3b3b4edc7a", + "scope": [ + "osf.full_read" + ] + }, + { + "name": "Another OAuth 2.0 Developer App", + "description": "See https://my.staging.app.io/about.", + "client_id": "3b4edc7aa9277d3b810045a8ffe5247b", + "scope": [ + "osf.full_write" + ] + } +] +``` + +
+ +### `POST /oauth2/revoke` + +#### Revoke One Token + +Handles revocation of refresh and access tokens. + +##### Request + +``` +https://accounts.osf.io/oauth2/revoke +``` + +##### `POST` Body Parameters + +Name | Value / Example +------- | --------------------------------------------------------------------------- +token | AT-6-0ckMxjkBHgs5PMqbCtg9BgFo49Y60A1bC5QxFnQeWdiWe9ZfvKwWS52jyIwLrrwVMGFxfa + +##### Response + +``` +HTTP 204 NO CONTENT +``` + +#### Revoke Tokens for a Client Application + +Revokes all tokens associated with a client application specified by the given client ID. + +##### Request + +``` +https://accounts.osf.io/oauth2/revoke +``` + +##### `POST` Body Parameters + +Parameter | Value / Example | Description +--------------- | ----------------------------------------- | ----------- +client_id | ffe5247b810045a8a9277d3b3b4edc7a | +client_secret | 5PgE96R3Z53dBuwBDkJfbK6ItDXvGhaxYpQ6r4cU | + +##### Response + +``` +HTTP 204 NO CONTENT +``` + +#### Revoke Tokens for a Resource User + +Revokes all tokens of a client application that have been issued to a resource user. The application is specified by the client ID and the user is specified by the principal ID associated with the access token. The token used for authorization must have been generated by the application *unless it is of token type `CAS`*. + +##### Request + +``` +https://accounts.osf.io/oauth2/revoke +``` + +##### Authorization Header + +Name | Value / Example +--------------- | ---------------------------------------------------------------------------------- +Authorization | Bearer AT-7-PvVw9wIcTOZYXFCVWbFhwsf9Q3idASiJeBdiWmLExcXSG54lCycokgCefWsy2Nzds4LoAW + +##### `POST` Body Parameters + +Parameter | Value / Example | Description +--------------- | ----------------------------------------- | ----------- +client_id | ffe5247b810045a8a9277d3b3b4edc7a | + +##### Response + +``` +HTTP 204 NO CONTENT +``` diff --git a/docs/osf-institutions-sso-via-cas.md b/docs/osf-institutions-sso-via-cas.md new file mode 100644 index 00000000..5bc4444b --- /dev/null +++ b/docs/osf-institutions-sso-via-cas.md @@ -0,0 +1,38 @@ +# Connecting to the Open Science Framework (OSF) via CAS-based Single Sign-On (SSO) + +COS's CAS-based SSO has limited functionality since it is just an alternative for institutions that can not use the Shibboleth-based SSO. Before proceeding, please read [Connecting to the Open Science Framework (OSF) via Shibboleth-based Single Sign-On (SSO)](https://github.com/CenterForOpenScience/cas-overlay/blob/develop/docs/osf-institutions-sso-via-saml.md) first for non-technical information on connecting to OSF via SSO. + +## Technical Implementation + +This SSO is based on [`cas-4.1.x`](https://github.com/apereo/cas/tree/4.1.x) and [`pac4j-1.7.x`](https://github.com/pac4j/pac4j/tree/1.7.x). Please refer to the [CAS protocol](https://apereo.github.io/cas/4.1.x/protocol/CAS-Protocol.html) and the [complete specification](https://apereo.github.io/cas/4.1.x/protocol/CAS-Protocol-Specification.html) for how CAS works. + +When connecting to the OSF via CAS-based SSO, COS's CAS system (OSF CAS) acts as the **CAS Client** and your institution's CAS system acts as the **CAS Server**. To implement and test SSO for your institution, please follow the steps below. + +### Registered Service + +Add OSF CAS domain / URL to your CAS system's **Registered Service** list and allow wildcard matching for query parameters. + +* Production: `https://accounts.osf.io/login?` +* Test / Staging: `https://accounts.test.osf.io/login?` + +### Authentication Endpoints + +Inform COS of the domain of your CAS system. More specifically, OSF CAS (as a client) expects the following endpoints to be available and functional. + +* Login: `/login` +* Logout: `/logout` +* Validation and attribute release: `/samlValidate` + +### Service Validation and Attribute Release + +OSF CAS makes a `POST` request to your CAS system's [`/samlValidate`](https://apereo.github.io/cas/4.1.x/protocol/CAS-Protocol-Specification.html#42-samlvalidate-cas-30) endpoint for ticket validation and attribute release. Please release the following required attributes and inform us of the attribute name for each. + +* Unique identifier for the user (e.g. `eppn`) +* User's institutional email (e.g. `mail`) +* User's full name (e.g. `displayName`) + +Please note that OSF CAS can not use your [`/p3/serviceValidate`](https://apereo.github.io/cas/4.1.x/protocol/CAS-Protocol-Specification.html#28-p3servicevalidate-cas-30) endpoint due to an old version of the library it uses, namely [`pac4j-1.7.x`](https://github.com/pac4j/pac4j/tree/1.7.x) and [`cas-server-support-pac4j-1.7.x`](https://github.com/apereo/cas/tree/4.1.x/cas-server-support-pac4j). In addition, OSF CAS does not use your [`/validate`](https://apereo.github.io/cas/4.1.x/protocol/CAS-Protocol-Specification.html#24-validate-cas-10) and [`/serviceValidate`](https://apereo.github.io/cas/4.1.x/protocol/CAS-Protocol-Specification.html#25-servicevalidate-cas-20) endpoints since these two can not release required attributes. + +### Test Accounts + +It is highly recommended that you can create a temporary institution test account for COS engineers (if possible), which will significantly aid and accelerate the process. diff --git a/docs/osf-institutions-sso-via-saml.md b/docs/osf-institutions-sso-via-saml.md new file mode 100644 index 00000000..c127ab79 --- /dev/null +++ b/docs/osf-institutions-sso-via-saml.md @@ -0,0 +1,55 @@ +# Connecting to the Open Science Framework (OSF) via Shibboleth-based Single Sign-On (SSO) + +This article provides general information about the COS's Shibboleth-based (SSO) integration for organizations who have signed the OSF for Institutions Offer of Services letter. + +## What is Single Sign-On? + +In general, Single Sign-On, or SSO, allows users authenticated with one trusted system (e.g. university network) to also authenticate using those same “home” credentials with another trusted network (e.g. OSF service). In the case of the second authentication, users are not asked to log in again, but instead the authenticated credentials are shared between systems. + +## Who can use Single Sign-On with Open Science Framework? + +Any organization that has implemented a SAML 2.0 Identity Provider (IdP) and signed the OSF for Institutions Offer for Services can offer SSO to OSF accounts. + +### A few notes: + +* Current OSF users who have already set up accounts with a different login, will be able to retain those credentials and choose to login with personal or institutional credentials. + +* Users’ authentication to the OSF service using SSO cannot also use the “forgot Password” link on the OSF website to remind them of their credentials, as their user credentials are specific to and managed by their organization. + +## Technical Implementation + +### InCommon Research & Scholarship Institutions + +COS is an [Research & Scholarship Entity Category (R&S)](https://refeds.org/category/research-and-scholarship) Service Provider (SP) registered by the [InCommon Federation](https://www.incommon.org/federation/). + +* Entity ID: `https://accounts.osf.io/shibboleth` +* Requested Attributes: `eduPersonPrincipalName` (SAML2), `mail` (SAML2) and `displayName` (SAML2) + +Full technical details can be found at https://www.incommon.org/federation/research-scholarship-adopters/. + +Please note that only COS's production SP is registered by InCommon. If you want to connect to COS's test / staging SP, here is the [SP metadata](https://accounts.test.osf.io/Shibboleth.sso/Metadata) as mentioned in **Other Institutions** below. + +### Other Institutions + +COS offers a Service Provider (SP) based on [SAML 2.0](https://docs.oasis-open.org/security/saml/Post2.0/sstc-saml-tech-overview-2.0.html) (the protocol) and [Shibboleth 2.0](https://wiki.shibboleth.net/confluence/display/SHIB2/Home) (the implementation). To implement and test SSO for your institution: + +* Ensure that your IT administrators have loaded COS's SP metadata into your IdP. + * Production: https://accounts.osf.io/Shibboleth.sso/Metadata + * Test and/or staging: https://accounts.test.osf.io/Shibboleth.sso/Metadata + +* Ensure that your IT administrators are releasing the three required pieces of information listed below and inform COS of the attributes you use for each of them. + * Unique identifier for the user (e.g. `eppn`) + * User's institutional email (e.g. `mail`) + * User's full name (e.g. `displayName` or **a pair of** `givenName` and `sn`) + + +### For All Institutions + +Inform COS of the user you would like to test with; your COS contact will ensure your account is ready to go and will send you a link to test the SSO configuration setup for your institution. + + +## Alternative SSO Options + +COS strongly recommends using this Shibboleth-based SSO when connecting to the OSF. However, if this is not available at your institution, please inform COS of alternative SSO options you have. We may support them in the future. + +One alternative that COS currently supports is the CAS-based SSO, please refer to [Connecting to the Open Science Framework (OSF) via CAS-based Single Sign-On (SSO)](https://github.com/CenterForOpenScience/cas-overlay/blob/develop/docs/osf-institutions-sso-via-cas.md) for technical details. diff --git a/docs/readme-old-config.md b/docs/readme-old-config.md new file mode 100644 index 00000000..c53d7093 --- /dev/null +++ b/docs/readme-old-config.md @@ -0,0 +1,50 @@ +# Deprecated README.md + +Information on this page are outdated and may no longer be correct. Please see the [README.md](https://github.com/CenterForOpenScience/cas-overlay/blob/develop/README.md) of the repository for latest information. + +--- + +Official Docs can be found [here](https://jasig.github.io/cas/) + +[CAS 4.1 Roadmap](https://wiki.jasig.org/display/CAS/CAS+4.1+Roadmap) + +[Docker Server](https://github.com/CenterForOpenScience/docker-library/tree/master/cas) + +## Configuration + +### JPA Ticket Registry + +* Postgres +* Apache DBCP2 (Database Connection Pooling v2) + +### Custom Application Authentication + +* Multi-Factor Authentication + * Time-based One Time Passwords (TOTP), e.g. Google Authenticator +* MongoDB authentication backend +* Customized login web flow prompts + * Login, Logout, One Time Password, Verification Key + * OAuth Application Approval +* [Login from external form](https://wiki.jasig.org/display/CAS/Using+CAS+from+external+link+or+custom+external+form) + +### Service Registry + +* Merging Service Registry Loader +* JSON Service Registry +* Open Science Framework Service Registry (MongoDB & OAuth) + +### Jetty 9.x Web Server + +* Startup Server Command + * `mvn -pl cas-server-webapp/ jetty:run` +* Optimized for faster builds + +If you have trouble building CAS via `mvn clean install`, you may need to install the "Java Cryptography Extension (JCE) Unlimited Strength +Jurisdiction Policy Files". Follow +[these instructions](http://bigdatazone.blogspot.com/2014/01/mac-osx-where-to-put-unlimited-jce-java.html) to unpack +the zip file, back up existing policy files, and install the new, stronger cryptography policy files. + +### TODO + +* Request Throttling +* Jetty JPA Shared Sessions diff --git a/etc/cas.properties b/etc/cas.properties index e1185a98..7d7a061f 100644 --- a/etc/cas.properties +++ b/etc/cas.properties @@ -68,8 +68,8 @@ cas.okstate.cas.protocol=SAML # OAuth Client: ORCiD oauth.orcid.authorize.url=https://orcid.org/oauth/authorize oauth.orcid.token.url=https://pub.orcid.org/oauth/token -oauth.orcid.client.id= -oauth.orcid.client.secret= +oauth.orcid.client.id=osf_orcid_developer_app_client_id +oauth.orcid.client.secret=osf_orcid_developer_app_client_secret oauth.orcid.member=false oauth.orcid.scope=/authenticate @@ -114,7 +114,7 @@ oauth.loginUrl=${server.name}/login #### This section "Central Authentication Service (CAS)" contains default properties from jasig/apereo. #### -server.name=http://localhost:8080 +server.name=http://192.168.168.167:8080 server.prefix=${server.name} # Spring Security's EL-based access rules for the /status URI of CAS that exposes health check information