Added Android code
[wl-app.git] / Android / app / src / main / java / com / moiseum / wolnelektury / connection / interceptors / OAuthSigningInterceptor.java
1 package com.moiseum.wolnelektury.connection.interceptors;
2
3 /**
4  * Created by Piotr Ostrowski on 06.06.2018.
5  */
6 /*
7  * Copyright (C) 2015 Jake Wharton
8  *
9  * Licensed under the Apache License, Version 2.0 (the "License");
10  * you may not use this file except in compliance with the License.
11  * You may obtain a copy of the License at
12  *
13  *      http://www.apache.org/licenses/LICENSE-2.0
14  *
15  * Unless required by applicable law or agreed to in writing, software
16  * distributed under the License is distributed on an "AS IS" BASIS,
17  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18  * See the License for the specific language governing permissions and
19  * limitations under the License.
20  */
21
22 import android.support.annotation.NonNull;
23 import android.util.Log;
24
25 import com.google.gson.Gson;
26 import com.google.gson.JsonSyntaxException;
27 import com.moiseum.wolnelektury.connection.models.OAuthTokenModel;
28
29 import java.io.IOException;
30 import java.net.URLEncoder;
31 import java.util.Map;
32 import java.util.Random;
33 import java.util.SortedMap;
34 import java.util.TreeMap;
35
36 import okhttp3.HttpUrl;
37 import okhttp3.Interceptor;
38 import okhttp3.MediaType;
39 import okhttp3.Request;
40 import okhttp3.RequestBody;
41 import okhttp3.Response;
42 import okhttp3.ResponseBody;
43 import okio.Buffer;
44 import okio.ByteString;
45
46 public final class OAuthSigningInterceptor implements Interceptor {
47         private static final String TAG = OAuthSigningInterceptor.class.getSimpleName();
48         private static final String REQUEST_TOKEN_HEADER = "Token-Requested";
49         private static final String AUTH_REQUIRED_HEADER = "Authentication-Required";
50         private static final String AUTHORIZATION_HEADER = "Authorization";
51
52         private static final String OAUTH_REALM = "realm=\"API\", ";
53         private static final String OAUTH_CONSUMER_KEY = "oauth_consumer_key";
54         private static final String OAUTH_NONCE = "oauth_nonce";
55         private static final String OAUTH_SIGNATURE = "oauth_signature";
56         private static final String OAUTH_SIGNATURE_METHOD = "oauth_signature_method";
57         private static final String OAUTH_SIGNATURE_METHOD_VALUE = "HMAC-SHA1";
58         private static final String OAUTH_TIMESTAMP = "oauth_timestamp";
59         private static final String OAUTH_ACCESS_TOKEN = "oauth_token";
60         private static final String OAUTH_VERSION = "oauth_version";
61         private static final String OAUTH_VERSION_VALUE = "1.0";
62         private static final long ONE_SECOND = 1000;
63
64         private final String consumerKey;
65         private final String consumerSecret;
66         private final Random random;
67         private String accessToken;
68         private String accessSecret;
69
70         public OAuthSigningInterceptor(String consumerKey, String consumerSecret, Random random) {
71                 this.consumerKey = consumerKey;
72                 this.consumerSecret = consumerSecret;
73                 this.random = random;
74         }
75
76         public void setToken(String accessToken, String accessSecret) {
77                 this.accessToken = accessToken;
78                 this.accessSecret = accessSecret;
79         }
80
81         @Override
82         public Response intercept(Chain chain) throws IOException {
83                 if (chain.request().header(REQUEST_TOKEN_HEADER) != null) {
84                         return handleRequestTokenRequest(chain);
85                 } else if (chain.request().header(AUTH_REQUIRED_HEADER) != null || isSignedIn()) {
86                         return chain.proceed(signRequest(chain.request()));
87                 } else {
88                         return chain.proceed(chain.request());
89                 }
90         }
91
92         private boolean isSignedIn() {
93                 return accessSecret != null && accessToken != null;
94         }
95
96         private Response handleRequestTokenRequest(Chain chain) throws IOException {
97                 Response tokenResponse = chain.proceed(requestTokenRequest(chain.request()));
98                 if (tokenResponse.isSuccessful() && tokenResponse.code() == 200 && tokenResponse.body() != null) {
99                         String jsonResponse = paramJson(tokenResponse.body().string());
100                         try {
101                                 Gson gson = new Gson();
102                                 OAuthTokenModel tokenModel = gson.fromJson(jsonResponse, OAuthTokenModel.class);
103                                 accessToken = tokenModel.getToken();
104                                 accessSecret = tokenModel.getTokenSecret();
105                                 return tokenResponse.newBuilder().body(ResponseBody.create(MediaType.parse("application/json"), jsonResponse)).build();
106                         } catch (JsonSyntaxException e) {
107                                 Log.v(TAG, "Failed to parse Oauth Request Token response.", e);
108                         }
109                 }
110                 return tokenResponse;
111         }
112
113         private Request signRequest(Request request) throws IOException {
114                 if (accessToken == null || accessSecret == null) {
115                         Log.e(TAG, "Missing authentication tokens, passing request unsigned.");
116                         return request;
117                 }
118
119                 SortedMap<String, String> parameters = getOAuthParams(request.url(), request.body());
120
121                 String baseUrl = request.url().newBuilder().query(null).build().toString();
122                 ByteString baseString = getBaseString(request.method(), baseUrl, parameters);
123                 String signingKey = utf8(consumerSecret) + "&" + (accessSecret != null ? utf8(accessSecret) : "");
124                 String signature = baseString.hmacSha1(ByteString.of(signingKey.getBytes())).base64();
125
126                 String authorization = "OAuth " + OAUTH_REALM
127                                 + OAUTH_CONSUMER_KEY + "=\"" + parameters.get(OAUTH_CONSUMER_KEY) + "\", "
128                                 + OAUTH_NONCE + "=\"" + parameters.get(OAUTH_NONCE) + "\", "
129                                 + OAUTH_SIGNATURE + "=\"" + signature + "\", "
130                                 + OAUTH_SIGNATURE_METHOD + "=\"" + OAUTH_SIGNATURE_METHOD_VALUE + "\", "
131                                 + OAUTH_TIMESTAMP + "=\"" + parameters.get(OAUTH_TIMESTAMP) + "\", "
132                                 + OAUTH_ACCESS_TOKEN + "=\"" + accessToken + "\", "
133                                 + OAUTH_VERSION + "=\"" + OAUTH_VERSION_VALUE + "\"";
134
135                 return request.newBuilder()
136                                 .addHeader(AUTHORIZATION_HEADER, authorization)
137                                 .build();
138         }
139
140         private Request requestTokenRequest(Request request) throws IOException {
141                 SortedMap<String, String> parameters = getOAuthParams(request.url(), request.body());
142
143                 String baseUrl = request.url().newBuilder().query(null).build().toString();
144                 ByteString baseString = getBaseString(request.method(), baseUrl, parameters);
145                 String signingKey = utf8(consumerSecret) + "&" + (accessSecret != null ? utf8(accessSecret) : "");
146                 String signature = baseString.hmacSha1(ByteString.of(signingKey.getBytes())).base64();
147
148                 HttpUrl.Builder urlBuilder = request.url().newBuilder()
149                                 .addQueryParameter(OAUTH_CONSUMER_KEY, parameters.get(OAUTH_CONSUMER_KEY))
150                                 .addQueryParameter(OAUTH_NONCE, parameters.get(OAUTH_NONCE))
151                                 .addQueryParameter(OAUTH_SIGNATURE_METHOD, OAUTH_SIGNATURE_METHOD_VALUE)
152                                 .addQueryParameter(OAUTH_TIMESTAMP, parameters.get(OAUTH_TIMESTAMP))
153                                 .addQueryParameter(OAUTH_VERSION, OAUTH_VERSION_VALUE)
154                                 .addQueryParameter(OAUTH_SIGNATURE, signature);
155                 if (accessToken != null) {
156                         urlBuilder.addQueryParameter(OAUTH_ACCESS_TOKEN, accessToken);
157                 }
158                 HttpUrl requestUrl = urlBuilder.build();
159
160                 return request.newBuilder().url(requestUrl).build();
161         }
162
163         private SortedMap<String, String> getOAuthParams(HttpUrl url, RequestBody requestBody) throws IOException {
164                 byte[] nonce = new byte[32];
165                 random.nextBytes(nonce);
166
167                 String oauthNonce = ByteString.of(nonce).base64().replaceAll("\\W", "");
168                 String oauthTimestamp = String.valueOf(System.currentTimeMillis() / ONE_SECOND);
169
170                 SortedMap<String, String> parameters = new TreeMap<>();
171                 parameters.put(OAUTH_CONSUMER_KEY, utf8(consumerKey));
172                 parameters.put(OAUTH_NONCE, oauthNonce);
173                 parameters.put(OAUTH_SIGNATURE_METHOD, OAUTH_SIGNATURE_METHOD_VALUE);
174                 parameters.put(OAUTH_TIMESTAMP, oauthTimestamp);
175                 parameters.put(OAUTH_VERSION, OAUTH_VERSION_VALUE);
176                 if (accessToken != null) {
177                         parameters.put(OAUTH_ACCESS_TOKEN, accessToken);
178                 }
179
180                 // Adding query params
181                 for (int i = 0; i < url.querySize(); i++) {
182                         parameters.put(utf8(url.queryParameterName(i)), utf8(url.queryParameterValue(i)));
183                 }
184
185                 // Adding body params
186                 if (requestBody != null) {
187                         Buffer body = new Buffer();
188                         requestBody.writeTo(body);
189
190                         while (!body.exhausted()) {
191                                 long keyEnd = body.indexOf((byte) '=');
192                                 if (keyEnd == -1) {
193                                         throw new IllegalStateException("Key with no value: " + body.readUtf8());
194                                 }
195                                 String key = body.readUtf8(keyEnd);
196                                 body.skip(1); // Equals.
197
198                                 long valueEnd = body.indexOf((byte) '&');
199                                 String value = valueEnd == -1 ? body.readUtf8() : body.readUtf8(valueEnd);
200                                 if (valueEnd != -1) {
201                                         body.skip(1); // Ampersand.
202                                 }
203
204                                 parameters.put(key, value);
205                         }
206                 }
207
208                 return parameters;
209         }
210
211         @NonNull
212         private ByteString getBaseString(String method, String baseUrl, SortedMap<String, String> parameters) throws IOException {
213                 Buffer base = new Buffer();
214                 base.writeUtf8(method);
215                 base.writeByte('&');
216                 base.writeUtf8(utf8(baseUrl));
217                 base.writeByte('&');
218
219                 boolean first = true;
220                 for (Map.Entry<String, String> entry : parameters.entrySet()) {
221                         if (!first) {
222                                 base.writeUtf8(utf8("&"));
223                         }
224                         first = false;
225                         base.writeUtf8(utf8(entry.getKey()));
226                         base.writeUtf8(utf8("="));
227                         base.writeUtf8(utf8(entry.getValue()));
228                 }
229                 return ByteString.of(base.readByteArray());
230         }
231
232         private String utf8(String escapedString) throws IOException {
233                 return URLEncoder.encode(escapedString, "UTF-8")
234                                 .replace("+", "%20")
235                                 .replace("*", "%2A")
236                                 .replace("%7E", "~");
237         }
238
239         private String paramJson(String paramIn) {
240                 paramIn = paramIn.replaceAll("=", "\":\"");
241                 paramIn = paramIn.replaceAll("&", "\",\"");
242                 return "{\"" + paramIn + "\"}";
243         }
244
245 }