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}