com.neovisionaries.android.oauth20.AuthorizationRequest Maven / Gradle / Ivy
Show all versions of nv-android-base Show documentation
/*
* Copyright (C) 2014 Neo Visionaries Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.neovisionaries.android.oauth20;
import static com.neovisionaries.android.util.ParcelUtils.readChar;
import static com.neovisionaries.android.util.ParcelUtils.readSerializableWithPresenceFlag;
import static com.neovisionaries.android.util.ParcelUtils.readStringWithPresenceFlag;
import static com.neovisionaries.android.util.ParcelUtils.writeChar;
import static com.neovisionaries.android.util.ParcelUtils.writeSerializableWithPresenceFlag;
import static com.neovisionaries.android.util.ParcelUtils.writeStringWithPresenceFlag;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLEncoder;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import android.os.Parcel;
import android.os.Parcelable;
/**
* OAuth 2.0 authorization request.
*
* @see RFC 6749 (OAuth 2.0), 3.1. Authorization Endpoint
*
* @see RFC 6749 (OAuth 2.0), 4.1.1. Authorization Request
*
* @see RFC 6749 (OAuth 2.0), 4.2.1. Authorization Request
*
* @since 1.9
*
* @author Takahiko Kawasaki
*/
public class AuthorizationRequest implements Parcelable
{
private URL mEndpoint;
private ResponseType mResponseType;
private String mClientId;
private URL mRedirectUri;
private Set mScopeSet;
private String mState;
private Map mExtraParameters;
private char mScopeDelimiter = ' ';
/**
* The default constructor.
*/
public AuthorizationRequest()
{
}
private AuthorizationRequest(Parcel in)
{
mEndpoint = (URL)readSerializableWithPresenceFlag(in);
mResponseType = (ResponseType)readSerializableWithPresenceFlag(in);
mClientId = readStringWithPresenceFlag(in);
mRedirectUri = (URL)readSerializableWithPresenceFlag(in);
mScopeSet = readScopeSet(in);
mState = readStringWithPresenceFlag(in);
mExtraParameters = readExtraParameters(in);
mScopeDelimiter = readChar(in);
}
/**
* Get the authorization endpoint.
*
* @return
* The authorization endpoint.
*
* @see RFC 6749 (OAuth 2.0), 3.1. Authorization Endpoint
*/
public URL getEndpoint()
{
return mEndpoint;
}
/**
* Set the authorization endpoint.
*
* @param endpoint
* The authorization endpoint.
*
* @return
* {@code this} object.
*
* @throws IllegalArgumentException
* The endpoint includes a fragment component.
* It is prohibited by the specification.
*
* @see RFC 6749 (OAuth 2.0), 3.1. Authorization Endpoint
*/
public AuthorizationRequest setEndpoint(URL endpoint)
{
if (endpoint != null)
{
validateEndpoint(endpoint);
}
mEndpoint = endpoint;
return this;
}
/**
* Set the authorization endpoint.
*
* @param endpoint
* The authorization endpoint.
*
* @return
* {@code this} object.
*
* @throws IllegalArgumentException
* The given endpoint is not a valid URL, or
* it includes a fragment component.
*
* @see RFC 6749 (OAuth 2.0), 3.1. Authorization Endpoint
*
* @since 1.10
*/
public AuthorizationRequest setEndpoint(String endpoint)
{
URL url = null;
if (endpoint != null)
{
try
{
url = new URL(endpoint);
}
catch (MalformedURLException e)
{
// The endpoint is malformed.
throw new IllegalArgumentException(e);
}
}
return setEndpoint(url);
}
/**
* Get the response type (= the value of {@code response_type}
* parameter (REQUIRED parameter)).
*
* @return
* The response type.
*
* @see RFC 6749 (OAuth 2.0), 3.1.1. Response Type
*/
public ResponseType getResponseType()
{
return mResponseType;
}
/**
* Set the response type (= the value of {@code response_type}
* parameter (REQUIRED parameter)).
*
* @param responseType
* The response type.
*
* @return
* {@code this} object.
*
* @see RFC 6749 (OAuth 2.0), 3.1.1. Response Type
*/
public AuthorizationRequest setResponseType(ResponseType responseType)
{
mResponseType = responseType;
return this;
}
/**
* Get the client ID (= the value of {@code client_id} parameter
* (REQUIRED parameter)).
*
* @return
* The client ID.
*/
public String getClientId()
{
return mClientId;
}
/**
* Set the client ID (= the value of {@code client_id} parameter
* (REQUIRED parameter)).
*
* @param clientId
* The client ID.
*
* @return
* {@code this} object.
*/
public AuthorizationRequest setClientId(String clientId)
{
mClientId = clientId;
return this;
}
/**
* Get the redirect URI (= the value of {@code redirect_uri} parameter
* (OPTIONAL parameter)).
*
* @return
* The redirect URI.
*
* @see RFC 6749 (OAuth 2.0), 3.1.2. Redirection Endpoint
*/
public URL getRedirectUri()
{
return mRedirectUri;
}
/**
* Set the redirect URI (= the value of {@code redirect_uri} parameter
* (OPTIONAL parameter)).
*
*
* RFC 6749 (OAuth 2.0), 3.1.2. Redirection Endpoint says
* "The redirection endpoint URI MUST be an absolute URI", so
* this method accepts the parameter value as {@link URL}.
*
*
* @param redirectUri
* The redirect URI.
*
* @return
* {@code this} object.
*
* @throws IllegalArgumentException
* The redirect URI failed to be converted into a {@link URL},
* or it includes a fragment component.
*
* @see RFC 6749 (OAuth 2.0), 3.1.2. Redirection Endpoint
*/
public AuthorizationRequest setRedirectUri(URL redirectUri)
{
if (redirectUri != null)
{
validateRedirectUri(redirectUri);
}
mRedirectUri = redirectUri;
return this;
}
/**
* Set the redirect URI (= the value of {@code redirect_uri} parameter
* (OPTIONAL parameter)).
*
*
* RFC 6749 (OAuth 2.0), 3.1.2. Redirection Endpoint says
* "The redirection endpoint URI MUST be an absolute URI".
*
*
* @param redirectUri
* The redirect URI.
*
* @return
* {@code this} object.
*
* @throws IllegalArgumentException
* The redirect URI includes a fragment component.
* It is prohibited by the specification.
*
* @see RFC 6749 (OAuth 2.0), 3.1.2. Redirection Endpoint
*
* @since 1.10
*/
public AuthorizationRequest setRedirectUri(String redirectUri)
{
URL url = null;
if (redirectUri != null)
{
try
{
url = new URL(redirectUri);
}
catch (MalformedURLException e)
{
// The redirect URI is malformed.
throw new IllegalArgumentException(e);
}
}
return setRedirectUri(url);
}
/**
* Get the scopes, which are elements in the value of
* {@code scope} parameter (OPTIONAL parameter).
*
* @return
* The scopes.
*/
public Set getScopeSet()
{
return mScopeSet;
}
/**
* Set the scopes, which are elements in the value of
* {@code scope} parameter (OPTIONAL parameter).
*
*
* Scopes must comply with the specification shown below.
* Otherwise, {@code IllegalArgumentException} is thrown.
*
*
*
* scope-token = 1*( %x21 / %x23-5B / %x5D-7E )
*
*
*
* {@code null} and an empty value do not raise
* {@code IllegalArgumentException} and are just ignored.
*
*
* @param scopeSet
* Scopes to be set.
*
* @return
* {@code this} object.
*
* @throws IllegalArgumentException
* There is a scope which does not comply with the
* specification.
*
* @see RFC 6749 (OAuth 2.0), 3.3. Access Token Scope
*/
public AuthorizationRequest setScopeSet(Set scopeSet)
{
mScopeSet = scopeSet;
if (scopeSet == null)
{
return this;
}
// Check whether all the scopes comply with the specification.
for (String scope : scopeSet)
{
if (scope == null || scope.length() == 0)
{
continue;
}
validateScope(scope);
}
return this;
}
/**
* Add scopes, which are elements in the value of
* {@code scope} parameter (OPTIONAL parameter).
*
*
* Scopes must comply with the specification shown below.
* Otherwise, {@code IllegalArgumentException} is thrown.
*
*
*
* scope-token = 1*( %x21 / %x23-5B / %x5D-7E )
*
*
*
* {@code null} and an empty value do not raise
* {@code IllegalArgumentException} and are just ignored.
*
*
* @param scopes
* Scopes to be added.
*
* @return
* {@code this} object.
*
* @throws IllegalArgumentException
* There is a scope which does not comply with the
* specification.
*
* @see RFC 6749 (OAuth 2.0), 3.3. Access Token Scope
*/
public AuthorizationRequest addScopes(String... scopes)
{
if (mScopeSet == null)
{
mScopeSet = new HashSet();
}
for (String scope : scopes)
{
if (scope == null || scope.length() == 0)
{
continue;
}
validateScope(scope);
mScopeSet.add(scope);
}
return this;
}
/**
* Remove scopes, which are elements in the value of
* {@code scope} parameter (OPTIONAL parameter).
*
* @param scopes
* Scopes to be removed.
*
* @return
* {@code this} object.
*
* @see RFC 6749 (OAuth 2.0), 3.3. Access Token Scope
*/
public AuthorizationRequest removeScopes(String... scopes)
{
if (mScopeSet == null)
{
return this;
}
for (String scope : scopes)
{
if (scope != null)
{
mScopeSet.remove(scope);
}
}
if (mScopeSet.size() == 0)
{
mScopeSet = null;
}
return this;
}
/**
* Get the state (= the value of {@code state} parameter
* (RECOMMENDED parameter)).
*
* @return
* The value of {@code state} parameter.
*/
public String getState()
{
return mState;
}
/**
* Set the state (= the value of {@code state} parameter
* (RECOMMENDED parameter)).
*
* @param state
* The value of {@code state} parameter.
*
* @return
* {@code this} object.
*/
public AuthorizationRequest setState(String state)
{
mState = state;
return this;
}
/**
* Get extra parameters added to the authorization request.
*
* @return
* Extra parameters.
*/
public Map getExtraParameters()
{
return mExtraParameters;
}
/**
* Set extra parameters added to the authorization request.
*
* @param parameters
* Extra parameters.
*
* @return
* {@code this} object.
*/
public AuthorizationRequest setExtraParameters(Map parameters)
{
mExtraParameters = parameters;
return this;
}
/**
* Add an extra parameter to the authorization request.
* For example, ({@code "display"}, {@code "touch"}) for Facebook.
*
* @param name
* Parameter name.
*
* @param value
* Parameter value.
*
* @return
* {@code this} object.
*/
public AuthorizationRequest addParameter(String name, String value)
{
if (name == null || name.length() == 0)
{
return this;
}
if (mExtraParameters == null)
{
mExtraParameters = new HashMap();
}
mExtraParameters.put(name, value);
return this;
}
/**
* Remove an extra parameter from the authorization request.
*
* @param name
* Parameter name.
*
* @return
* {@code this} object.
*/
public AuthorizationRequest removeParameter(String name)
{
if (mExtraParameters == null)
{
return this;
}
if (name == null)
{
return this;
}
mExtraParameters.remove(name);
if (mExtraParameters.size() == 0)
{
mExtraParameters = null;
}
return this;
}
/**
* Get the delimiter for scopes.
* The default value is a space (0x20).
*
* @return
* The scope delimiter.
*/
public char getScopeDelimiter()
{
return mScopeDelimiter;
}
/**
* Set the delimiter for scopes.
*
*
* RFC 6749 (OAuth 2.0), 3.3. Access Token Scope says
* "The value of the scope parameter is expressed as a list of
* space-delimited, case-sensitive strings."
* However, Facebook does not comply with this specification
* and uses a comma (0x2C) as the delimiter. Therefore, for
* Facebook, {@code setScopeDelimiter(',')} needs to be called.
*
*
* @param delimiter
* The scope
*
* @return
* {@code this} object.
*
* @see RFC 6749 (OAuth 2.0), 3.3. Access Token Scope
*/
public AuthorizationRequest setScopeDelimiter(char delimiter)
{
mScopeDelimiter = delimiter;
return this;
}
private void validateEndpoint(URL endpoint)
{
if (endpoint.getRef() != null)
{
throw new IllegalArgumentException("Endpoint must not include a fragment component.");
}
}
private void validateRedirectUri(URL redirectUri)
{
if (redirectUri.getRef() != null)
{
throw new IllegalArgumentException("Redirect URI must not include a fragment component.");
}
}
private void validateScope(String scope)
{
int len = scope.length();
for (int i = 0; i < len; ++i)
{
char ch = scope.charAt(i);
if (ch < 0x21 || 0x7E < ch || ch == 0x22 || ch == 0x5C)
{
throw new IllegalArgumentException("Bad scope: " + scope);
}
}
}
private void validate()
{
if (mEndpoint == null)
{
throw new IllegalStateException("Endpoint is not set.");
}
if (mResponseType == null)
{
throw new IllegalStateException("Response type is not set.");
}
if (mClientId == null)
{
throw new IllegalStateException("Client ID is not set.");
}
}
/**
* Convert this authorization request to a URL.
*
* @return
* The URL that represents this authorization request.
*
* @throws IllegalStateException
*
* - Endpoint is not set.
*
- Response type is not set.
*
- Client ID is not set.
*
*/
public URL toURL()
{
// Validate parameters.
validate();
// Query component.
String query = buildQuery();
String url = new StringBuilder()
.append(mEndpoint.getProtocol())
.append("://")
.append(mEndpoint.getAuthority())
.append(mEndpoint.getPath())
.append("?")
.append(query)
.toString();
try
{
return new URL(url);
}
catch (MalformedURLException e)
{
// This should not happen.
e.printStackTrace();
return null;
}
}
private String buildQuery()
{
StringBuilder queryBuilder = new StringBuilder();
// Get the query component of the authorization endpoint.
String query = mEndpoint.getQuery();
// If the authorization endpoint includes a query component.
if (query != null)
{
queryBuilder.append(query).append("&");
}
// response_type (REQUIRED)
queryBuilder.append("response_type=").append(mResponseType.name());
// client_id (REQUIRED)
queryBuilder.append("&client_id=").append(urlEncode(mClientId));
// redirect_uri (OPTIONAL)
if (mRedirectUri != null)
{
queryBuilder.append("&redirect_uri=").append(urlEncode(mRedirectUri.toString()));
}
// scope (OPTIONAL)
if (mScopeSet != null)
{
String scope = buildScope();
queryBuilder.append("&scope=").append(urlEncode(scope));
}
// state (RECOMMENDED)
if (mState != null)
{
queryBuilder.append("&state=").append(urlEncode(mState));
}
// Extra parameters.
if (mExtraParameters != null)
{
appendExtraParameters(queryBuilder);
}
return queryBuilder.toString();
}
private String buildScope()
{
StringBuilder scopeBuilder = new StringBuilder();
for (String scope : mScopeSet)
{
scopeBuilder.append(scope).append(mScopeDelimiter);
}
if (scopeBuilder.length() != 0)
{
// Remove the space at the end.
scopeBuilder.setLength(scopeBuilder.length() - 1);
}
return scopeBuilder.toString();
}
private void appendExtraParameters(StringBuilder builder)
{
for (Map.Entry entry : mExtraParameters.entrySet())
{
String name = entry.getKey();
if (name == null || name.length() == 0)
{
continue;
}
String value = entry.getValue();
if (value == null)
{
value = "";
}
builder
.append("&")
.append(urlEncode(name))
.append("=")
.append(urlEncode(value));
}
}
private static String urlEncode(String value)
{
try
{
return URLEncoder.encode(value, "UTF-8");
}
catch (UnsupportedEncodingException e)
{
// This never happens.
return value;
}
}
public int describeContents()
{
return 0;
}
public void writeToParcel(Parcel out, int flags)
{
writeSerializableWithPresenceFlag(out, mEndpoint);
writeSerializableWithPresenceFlag(out, mResponseType);
writeStringWithPresenceFlag(out, mClientId);
writeSerializableWithPresenceFlag(out, mRedirectUri);
writeScopeSet(out, mScopeSet);
writeStringWithPresenceFlag(out, mState);
writeExtraParameters(out, mExtraParameters);
writeChar(out, mScopeDelimiter);
}
private Set readScopeSet(Parcel in)
{
// Read the size.
int size = in.readInt();
if (size == 0)
{
return null;
}
Set set = new HashSet(size);
for (int i = 0; i < size; ++i)
{
set.add(in.readString());
}
return set;
}
private void writeScopeSet(Parcel out, Set set)
{
if (set == null)
{
// Not present (size = 0).
out.writeInt(0);
return;
}
int size = set.size();
// Write the size.
out.writeInt(size);
for (String value : set)
{
out.writeString(value);
}
}
private Map readExtraParameters(Parcel in)
{
// Read the size.
int size = in.readInt();
if (size == 0)
{
return null;
}
Map map = new HashMap(size);
for (int i = 0; i < size; ++i)
{
String key = in.readString();
String value = in.readString();
map.put(key, value);
}
return map;
}
private void writeExtraParameters(Parcel out, Map map)
{
if (map == null)
{
// Not present (size = 0).
out.writeInt(0);
return;
}
int size = map.size();
// Write the size.
out.writeInt(size);
for (Map.Entry entry : map.entrySet())
{
out.writeString(entry.getKey());
out.writeString(entry.getValue());
}
}
/**
* CREATER required by {@link Parcelable} interface.
*/
public static final Parcelable.Creator CREATOR
= new Parcelable.Creator()
{
public AuthorizationRequest createFromParcel(Parcel in)
{
return new AuthorizationRequest(in);
}
public AuthorizationRequest[] newArray(int size)
{
return new AuthorizationRequest[size];
}
};
}