001/*
002 * flattr4j - A Java library for Flattr
003 *
004 * Copyright (C) 2011 Richard "Shred" Körber
005 *   http://flattr4j.shredzone.org
006 *
007 * This program is free software: you can redistribute it and/or modify
008 * it under the terms of the GNU General Public License / GNU Lesser
009 * General Public License as published by the Free Software Foundation,
010 * either version 3 of the License, or (at your option) any later version.
011 *
012 * Licensed under the Apache License, Version 2.0 (the "License");
013 * you may not use this file except in compliance with the License.
014 *
015 * This program is distributed in the hope that it will be useful,
016 * but WITHOUT ANY WARRANTY; without even the implied warranty of
017 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
018 */
019package org.shredzone.flattr4j.oauth;
020
021import java.io.UnsupportedEncodingException;
022import java.net.URLEncoder;
023import java.util.EnumSet;
024
025import org.shredzone.flattr4j.connector.Connection;
026import org.shredzone.flattr4j.connector.Connector;
027import org.shredzone.flattr4j.connector.FlattrObject;
028import org.shredzone.flattr4j.connector.RequestType;
029import org.shredzone.flattr4j.connector.impl.FlattrConnector;
030import org.shredzone.flattr4j.exception.FlattrException;
031
032/**
033 * Helps through the OAuth2 authentication process at Flattr.
034 *
035 * @author Richard "Shred" Körber
036 * @see <a href="http://tools.ietf.org/html/draft-ietf-oauth-v2-21">IETF OAuth V2</a>
037 */
038public class FlattrAuthenticator {
039    private static final String ENCODING = "utf-8";
040
041    private final ConsumerKey consumerKey;
042
043    private String requestTokenUrl = "https://flattr.com/oauth/authorize";
044    private String accessTokenUrl = "https://flattr.com/oauth/token";
045    private String responseType = "code";
046
047    private String callbackUrl = null;
048
049    private EnumSet<Scope> scope = EnumSet.noneOf(Scope.class);
050
051    /**
052     * The request token url.
053     * <p>
054     * <em>NOTE:</em> This request token url must match the url defined at
055     * http://developers.flattr.net/api/#authorization
056     *
057     * @return the defined request token url
058     */
059    public String getRequestTokenUrl()      { return requestTokenUrl; }
060    public void setRequestTokenUrl(String requestTokenUrl) { this.requestTokenUrl = requestTokenUrl; }
061
062    /**
063     * The access token url required to retrieve the access token for flattr.com.
064     * <p>
065     * <em>NOTE:</em> This access token url must match the url defined at
066     * http://developers.flattr.net/api/#authorization
067     *
068     * @return the defined access token url
069     */
070    public String getAccessTokenUrl()       { return accessTokenUrl; }
071    public void setAccessTokenUrl(String accessTokenUrl) { this.accessTokenUrl = accessTokenUrl; }
072
073    /**
074     * A callback URL. If set, the user is forwarded to this URL in order to pass the
075     * valication code. {@code null} means that the user is required to retrieve the code
076     * from Flattr and enter it manually. Use {@code null} if you cannot provide a
077     * callback URL, for example on a desktop or handheld device application.
078     * <p>
079     * <em>NOTE:</em> This callback URL must <em>exactly</em> match the URL that was used
080     * on registration. Otherwise the authentication process will fail.
081     * <em>NOTE:</em> If you registered your application as "client" type, callback url
082     * <em>must be</em> {@code null}.
083     * <p>
084     * Defaults to {@code null}.
085     */
086    public String getCallbackUrl()          { return callbackUrl; }
087    public void setCallbackUrl(String callbackUrl) { this.callbackUrl = callbackUrl; }
088
089    /**
090     * The access scope. This is a set of rights the consumer needs. The set of rights
091     * is shown to the user on authentication. Defaults to an empty set.
092     */
093    public EnumSet<Scope> getScope()        { return scope; }
094    public void setScope(EnumSet<Scope> scope) { this.scope = scope; }
095
096    /**
097     * The OAuth response type. This is {@code code} or {@code token}, with the former
098     * being the default.
099     *
100     * @since 2.3
101     */
102    public void setResponseType(String responseType) { this.responseType = responseType; }
103    public String getResponseType()         { return responseType; }
104
105    /**
106     * Constructs a new instance with the given {@link ConsumerKey}.
107     *
108     * @param consumerKey {@link ConsumerKey}
109     */
110    public FlattrAuthenticator(ConsumerKey consumerKey) {
111        this.consumerKey = consumerKey;
112    }
113
114    /**
115     * Constructs a new instance with the given consumer key and secret.
116     *
117     * @param key
118     *            Consumer key
119     * @param secret
120     *            Consumer secret
121     */
122    public FlattrAuthenticator(String key, String secret) {
123        this(new ConsumerKey(key, secret));
124    }
125
126    /**
127     * Authenticates this application against Flattr. The user is required to visit the
128     * returned url and retrieve a code. The code needs to be passed to
129     * {@link #fetchAccessToken(String)} in order to complete the authorization.
130     * <p>
131     * When a callback url was set, Flattr will forward the user to this url with the
132     * following GET parameter:
133     * <ul>
134     * <li>{@code code}: the code</li>
135     * </ul>
136     * This way the user only needs to log in at Flattr, but does not need to copy a code
137     * to complete the authorization.
138     * <p>
139     * Scope flags need to be set properly before invocation.
140     *
141     * @return The authentication URL that the user must visit
142     * @since 2.0
143     */
144    public String authenticate() throws FlattrException {
145        try {
146            StringBuilder url = new StringBuilder();
147            url.append(requestTokenUrl);
148            url.append("?response_type=").append(URLEncoder.encode(responseType, ENCODING));
149            url.append("&client_id=").append(URLEncoder.encode(consumerKey.getKey(), ENCODING));
150
151            if (callbackUrl != null) {
152                url.append("&redirect_uri=").append(URLEncoder.encode(callbackUrl, ENCODING));
153            }
154
155            if (!scope.isEmpty()) {
156                url.append("&scope=").append(URLEncoder.encode(buildScopeString(), ENCODING));
157            }
158
159            return url.toString();
160        } catch (UnsupportedEncodingException ex) {
161            // should never be thrown, as "utf-8" encoding is available on any VM
162            throw new IllegalStateException(ex);
163        }
164    }
165
166    /**
167     * Fetches an {@link AccessToken} that gives access to the Flattr API. After the user
168     * entered the code (or when the callback url was invoked), this method is invoked to
169     * complete the authorization process.
170     * <p>
171     * The returned access token can be serialized or the {@link AccessToken#getToken()}
172     * persisted in a database. It is needed to access the Flattr API with a valid
173     * authentication. The token is valid until revoked by the user.
174     *
175     * @param code
176     *            The code that was returned from Flattr
177     * @return {@link AccessToken} giving access to the Flattr API for the authenticated
178     *         user
179     */
180    public AccessToken fetchAccessToken(String code) throws FlattrException {
181        Connector connector = createConnector();
182
183        Connection conn = connector.create(RequestType.POST)
184                .url(accessTokenUrl)
185                .key(consumerKey)
186                .form("code", code)
187                .form("grant_type", "authorization_code");
188
189        if (callbackUrl != null) {
190            conn.form("redirect_uri", callbackUrl);
191        }
192
193        FlattrObject response = conn.singleResult();
194        String accessToken = response.get("access_token");
195        String tokenType = response.get("token_type");
196
197        if (!"bearer".equalsIgnoreCase(tokenType)) {
198            throw new FlattrException("Unknown token type " + tokenType);
199        }
200
201        return new AccessToken(accessToken);
202    }
203
204    /**
205     * Creates a {@link Connector} for sending requests.
206     *
207     * @return {@link Connector}
208     */
209    protected Connector createConnector() {
210        return new FlattrConnector();
211    }
212
213    /**
214     * Builds a scope string for the scope flags set.
215     *
216     * @return Scope string: scope texts, separated by spaces.
217     */
218    protected String buildScopeString() {
219        StringBuilder sb = new StringBuilder();
220
221        if (scope.contains(Scope.FLATTR      )) sb.append(" flattr");
222        if (scope.contains(Scope.THING       )) sb.append(" thing");
223        if (scope.contains(Scope.EMAIL       )) sb.append(" email");
224        if (scope.contains(Scope.EXTENDEDREAD)) sb.append(" extendedread");
225
226        return sb.toString().trim();
227    }
228
229}