You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

371 lines
15 KiB

package com.github.jreddit.oauth;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import javax.xml.bind.DatatypeConverter;
import org.apache.oltu.oauth2.client.OAuthClient;
import org.apache.oltu.oauth2.client.URLConnectionClient;
import org.apache.oltu.oauth2.client.request.OAuthClientRequest;
import org.apache.oltu.oauth2.client.request.OAuthClientRequest.TokenRequestBuilder;
import org.apache.oltu.oauth2.common.OAuthProviderType;
import org.apache.oltu.oauth2.common.exception.OAuthProblemException;
import org.apache.oltu.oauth2.common.exception.OAuthSystemException;
import org.apache.oltu.oauth2.common.message.types.GrantType;
import com.github.jreddit.oauth.app.RedditApp;
import com.github.jreddit.oauth.exception.RedditOAuthException;
import com.github.jreddit.oauth.param.RedditDuration;
import com.github.jreddit.oauth.param.RedditScopeBuilder;
import com.github.jreddit.request.util.KeyValueFormatter;
/**
* Thread-safe reddit OAuth agent.<br>
* <br>
* Communicates with reddit to retrieve tokens, and converts them
* into {@link RedditToken}s, which are used internally by jReddit. This class
* supports both the <i>code grant flow</i> and <i>implicit grant flow</i>.
*
* @author Simon Kassing
*/
public class RedditOAuthAgent {
/** Reddit authorization endpoint. */
private static final String REDDIT_AUTHORIZE = "https://www.reddit.com/api/v1/authorize?";
/** Grant type for an installed client (weirdly enough a URI). */
private static final String GRANT_TYPE_INSTALLED_CLIENT = "https://oauth.reddit.com/grants/installed_client";
/** Grant type for client credentials (described in OAuth2 standard). */
private static final String GRANT_TYPE_CLIENT_CREDENTIALS = "client_credentials";
/* Parameter keys */
private static final String PARAM_CLIENT_ID = "client_id";
private static final String PARAM_RESPONSE_TYPE = "response_type";
private static final String PARAM_STATE = "state";
private static final String PARAM_REDIRECT_URI = "redirect_uri";
private static final String PARAM_DURATION = "duration";
private static final String PARAM_SCOPE = "scope";
private static final String PARAM_GRANT_TYPE = "grant_type";
private static final String PARAM_CODE = "code";
private static final String PARAM_DEVICE_ID = "device_id";
/* Header keys */
private static final String HEADER_USER_AGENT = "User-Agent";
private static final String HEADER_AUTHORIZATION = "Authorization";
/** User agent. */
private final String userAgent;
/** OAuth2 client for OAuth related requests. */
private OAuthClient oAuthClient;
/** Reddit application. */
private RedditApp redditApp;
/**
* Constructor for a Reddit OAuth agent.<br>
* <br>
* A default Apache OAuthClient will be made to perform the OAuth communication.
*
* @param userAgent User agent for your application (e.g. "jReddit: Reddit API Wrapper for Java")
* @param redditApp Reddit application
*/
public RedditOAuthAgent(String userAgent, RedditApp redditApp) {
this(userAgent, redditApp, new OAuthClient(new URLConnectionClient()));
}
/**
* Constructor for a Reddit OAuth agent.
*
* @param userAgent User agent for your application (e.g. "jReddit: Reddit API Wrapper for Java")
* @param redditApp Reddit application
* @param oAuthClient Apache OAuth2 client
*/
public RedditOAuthAgent(String userAgent, RedditApp redditApp, OAuthClient oAuthClient) {
this.userAgent = userAgent;
this.redditApp = redditApp;
this.oAuthClient = oAuthClient;
}
/**
* Generate the <i>code flow</i> Uniform Resource Locator (URI) for a
* reddit user to authorize your application.<br>
* <br>
* The user will, after authorization, receive a <i>code</i>. This can be turned into
* a <i>RedditToken</i> using {@link #token(String)}.
*
* @param scopeBuilder Authorization scope builder (must not be <i>null</i>)
* @param duration Duration that the token can last
*
* @return The URI users need to visit and retrieve the <i>code</i> from
*
* @see {@link #token(String)} for converting the <i>code</i> into a usable <i>RedditToken</i>
*/
public synchronized String generateCodeFlowURI(RedditScopeBuilder scopeBuilder, RedditDuration duration) {
// Set parameters
Map<String, String> params = new HashMap<String, String>();
params.put(PARAM_CLIENT_ID, redditApp.getClientID());
params.put(PARAM_RESPONSE_TYPE, "code");
params.put(PARAM_STATE, UUID.randomUUID().toString());
params.put(PARAM_REDIRECT_URI, redditApp.getRedirectURI());
params.put(PARAM_DURATION, duration.value());
params.put(PARAM_SCOPE, scopeBuilder.build());
// Create URI
return REDDIT_AUTHORIZE + KeyValueFormatter.format(params, true);
}
/**
* Generate the <i>implicit flow</i> Uniform Resource Locator (URI) for a
* reddit user to authorize your application.<br>
* <br>
* The user will, after authorization, receive token information. This can be turned into
* a <i>RedditToken</i> using {@link #tokenFromInfo(String, String, long, String)}.
*
* @param scopeBuilder Authorization scope builder (must not be <i>null</i>)
*
* @return The URI users need to visit and retrieve the <i>token information</i> from
*
* @see {@link #tokenFromInfo(String, String, long, String)} for converting the
* <i>token information</i> into <i>RedditToken</i>
*/
public synchronized String generateImplicitFlowURI(RedditScopeBuilder scopeBuilder) {
// Set parameters
Map<String, String> params = new HashMap<String, String>();
params.put(PARAM_CLIENT_ID, redditApp.getClientID());
params.put(PARAM_RESPONSE_TYPE, "token");
params.put(PARAM_STATE, UUID.randomUUID().toString());
params.put(PARAM_REDIRECT_URI, redditApp.getRedirectURI());
params.put(PARAM_SCOPE, scopeBuilder.build());
// Create URI
return REDDIT_AUTHORIZE + KeyValueFormatter.format(params, true);
}
/**
* Add a user agent to the OAuth request.
*
* @param request OAuth request
*/
private void addUserAgent(OAuthClientRequest request) {
request.addHeader(HEADER_USER_AGENT, userAgent);
}
/**
* Add the basic authentication protocol to the OAuth request using
* the credentials of the Reddit application provided.
*
* @param request OAuth request
* @param app Reddit application
*/
private void addBasicAuthentication(OAuthClientRequest request, RedditApp app) {
String authString = app.getClientID() + ":" + app.getClientSecret();
String authStringEnc = DatatypeConverter.printBase64Binary(authString.getBytes());
request.addHeader(HEADER_AUTHORIZATION, "Basic " + authStringEnc);
}
/**
* Token retrieval (<i>code</i> flow).<br>
* <br>
* Retrieve a token for a specific user, meaning that the token is <u>coupled to a user</u>.
* After it has expired, the token will no longer work. You must either request a new
* token, or refresh it using {@link #refreshToken(RedditToken)}.
*
* @param code One-time code received from the user, after manual authorization by that user
*
* @return Token (associated with a user)
*
* @throws RedditOAuthException
*/
public synchronized RedditToken token(String code) throws RedditOAuthException {
try {
// Set general values of the request
OAuthClientRequest request = OAuthClientRequest
.tokenProvider(OAuthProviderType.REDDIT)
.setGrantType(GrantType.AUTHORIZATION_CODE)
.setClientId(redditApp.getClientID())
.setClientSecret(redditApp.getClientSecret())
.setRedirectURI(redditApp.getRedirectURI())
.setParameter(PARAM_CODE, code)
.buildBodyMessage();
// Add the user agent
addUserAgent(request);
// Add basic authentication
addBasicAuthentication(request, redditApp);
// Return a wrapper controlled by jReddit
return new RedditToken(oAuthClient.accessToken(request));
} catch (OAuthSystemException oase) {
throw new RedditOAuthException(oase);
} catch (OAuthProblemException oape) {
throw new RedditOAuthException(oape);
}
}
/**
* Refresh token.<br>
* <br>
* This is <u>only</u> possible for tokens retrieved through the <u>code flow</u>
* authorization and had their duration set to permanent. Tokens that do not have
* a refresh_token with them or are expired, will not be able to be refreshed.
* In that case, a new one must be acquired.
*
* @param rToken Reddit token (which needs to be refreshed)
*
* @return Whether the token was successfully refreshed
*
* @throws RedditOAuthException
*
* @see RedditToken#isRefreshable()
*/
public synchronized boolean refreshToken(RedditToken rToken) throws RedditOAuthException {
try {
// Check whether the token can be refreshed
if (rToken.isRefreshable()) {
// Set general values of the request
OAuthClientRequest request = OAuthClientRequest
.tokenProvider(OAuthProviderType.REDDIT)
.setGrantType(GrantType.REFRESH_TOKEN)
.setRefreshToken(rToken.getRefreshToken())
.buildBodyMessage();
// Add the user agent
addUserAgent(request);
// Add basic authentication
addBasicAuthentication(request, redditApp);
// Return a wrapper controlled by jReddit
rToken.refresh(oAuthClient.accessToken(request));
return true;
} else {
// The token cannot be refreshed
return false;
}
} catch (OAuthSystemException oase) {
throw new RedditOAuthException(oase);
} catch (OAuthProblemException oape) {
throw new RedditOAuthException(oape);
}
}
/**
* Token retrieval (<i>app-only</i> flow).<br>
* <br>
* Retrieve a token for the <u>application-only</u>, meaning that the
* token is <u>not coupled to any user</u>. The token is typically only
* <u>valid for a short period of time</u> (at the moment of writing: 1 hour).
* After it has expired, the token will no longer work. You must request a <u>new</u>
* token in that case. Refreshing an application-only token is not possible.
*
* @param confidential <i>True</i>: confidential clients (web apps / scripts) not acting on
* behalf of one or more logged out users. <i>False</i>: installed app types,
* and other apps acting on behalf of one or more logged out users.
*
* @return Token (not associated with a user)
*
* @throws RedditOAuthException
*/
public synchronized RedditToken tokenAppOnly(boolean confidential) throws RedditOAuthException {
try {
// Set general values of the request
TokenRequestBuilder builder = OAuthClientRequest
.tokenProvider(OAuthProviderType.REDDIT)
.setParameter(PARAM_GRANT_TYPE, confidential ? GRANT_TYPE_CLIENT_CREDENTIALS : GRANT_TYPE_INSTALLED_CLIENT)
.setClientId(redditApp.getClientID())
.setClientSecret(redditApp.getClientSecret());
// If it is not acting on behalf of a unique client, a unique device identifier must be generated:
if (!confidential) {
builder = builder.setParameter(PARAM_DEVICE_ID, UUID.randomUUID().toString());
}
// Build the request
OAuthClientRequest request = builder.buildBodyMessage();
// Add the user agent
addUserAgent(request);
// Add basic authentication
addBasicAuthentication(request, redditApp);
// Return a wrapper controlled by jReddit
return new RedditToken(oAuthClient.accessToken(request));
} catch (OAuthSystemException oase) {
throw new RedditOAuthException(oase);
} catch (OAuthProblemException oape) {
throw new RedditOAuthException(oape);
}
}
/**
* Generate a token from information received using the <i>implicit grant flow</i>.<br>
* <br>
* <b>WARNING:</b> The expiration of the token is no longer very accurate. There is a delay
* between the user receiving the token, and inputting it into this function. Beware that the
* token might expire earlier than that the token reports it to.
*
* @param accessToken Access token
* @param tokenType Token type (commonly "bearer")
* @param expiresIn Expires in (seconds)
* @param scope Scope
*
* @return <i>RedditToken</i> generated using the given information.
*/
public synchronized RedditToken tokenFromInfo(String accessToken, String tokenType, long expiresIn, String scope) {
return new RedditToken(accessToken, tokenType, expiresIn, scope);
}
/**
* Revocation of a <i>RedditToken</i>.<br>
* <br>
* Be sure to not use the token after
* calling this function, as its state pertaining its validity (e.g. scope,
* expiration, refreshability) is no longer valid when it is revoked.<br>
* <br>
* <i>Note: Per RFC 7009, this request will return a success (204) response
* even if the passed in token was never valid.</i>
*
* @param token <i>RedditToken</i> to revoke
* @param revokeAccessTokenOnly Whether to only revoke the access token, or both
*
* @return Whether the token is no longer valid
*/
public boolean revoke(RedditToken token, boolean revokeAccessTokenOnly) {
// TODO: Implement
// https://www.reddit.com/api/v1/revoke_token
// In POST data: token=TOKEN&token_type_hint=TOKEN_TYPE
// TOKEN_TYPE: refresh_token or access_token
//
return true;
}
}