OAuth-2 authentication with full name / e-mail lookup.
Instead of requiring manual Gerrit user profile registration,
the Full name and EMail are automatically retrieved at
login and stored in Gerrit as read-only.
Profile is automatically re-synched at next login
on by clicking the "Reload" button on the Gerrit
settings page.
Change-Id: I952e8e79d05f3ba102b012958afda31dd979d658
diff --git a/github-oauth/src/main/java/com/googlesource/gerrit/plugins/github/oauth/AuthenticatedHttpRequest.java b/github-oauth/src/main/java/com/googlesource/gerrit/plugins/github/oauth/AuthenticatedHttpRequest.java
index ccb2d5c..46f6ea9 100644
--- a/github-oauth/src/main/java/com/googlesource/gerrit/plugins/github/oauth/AuthenticatedHttpRequest.java
+++ b/github-oauth/src/main/java/com/googlesource/gerrit/plugins/github/oauth/AuthenticatedHttpRequest.java
@@ -14,66 +14,45 @@
package com.googlesource.gerrit.plugins.github.oauth;
import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.HashSet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
-import com.google.common.base.Objects;
+import com.google.common.collect.Iterators;
public class AuthenticatedHttpRequest extends HttpServletRequestWrapper {
- private String httpHeaderName;
- private String httpHeaderValue;
+ private HashMap<String, String> headers = new HashMap<String, String>();
public AuthenticatedHttpRequest(HttpServletRequest request,
- String authHeaderName, String authHeaderValue) {
+ String... headerNamesValues) {
super(request);
- this.httpHeaderName = authHeaderName;
- this.httpHeaderValue = authHeaderValue;
+
+ for (int i = 0; i < headerNamesValues.length;) {
+ String name = headerNamesValues[i++];
+ String value = headerNamesValues[i++];
+ if (name != null && value != null) {
+ headers.put(name, value);
+ }
+ }
}
@Override
public Enumeration<String> getHeaderNames() {
-
final Enumeration<String> wrappedHeaderNames = super.getHeaderNames();
- return new Enumeration<String>() {
-
- boolean lastElement;
- boolean headerFound;
-
- @Override
- public boolean hasMoreElements() {
- if (wrappedHeaderNames.hasMoreElements()) {
- return true;
- } else if (!lastElement && !headerFound) {
- return true;
- } else {
- return false;
- }
- }
-
- @Override
- public String nextElement() {
- if (wrappedHeaderNames.hasMoreElements()) {
- String nextHeader = wrappedHeaderNames.nextElement();
- if (nextHeader.equalsIgnoreCase(httpHeaderName)) {
- headerFound = true;
- }
- return nextHeader;
- } else if (!lastElement && !headerFound) {
- lastElement = true;
- return httpHeaderName;
- } else {
- return null;
- }
- }
-
- };
+ HashSet<String> headerNames = new HashSet<String>(headers.keySet());
+ while (wrappedHeaderNames.hasMoreElements()) {
+ headerNames.add(wrappedHeaderNames.nextElement());
+ }
+ return Iterators.asEnumeration(headerNames.iterator());
}
@Override
public String getHeader(String name) {
- if (name.equalsIgnoreCase(httpHeaderName)) {
- return httpHeaderValue;
+ String headerValue = headers.get(name);
+ if (headerValue != null) {
+ return headerValue;
} else {
return super.getHeader(name);
}
diff --git a/github-oauth/src/main/java/com/googlesource/gerrit/plugins/github/oauth/GitHubLogin.java b/github-oauth/src/main/java/com/googlesource/gerrit/plugins/github/oauth/GitHubLogin.java
index 50018ea..69ceaec 100644
--- a/github-oauth/src/main/java/com/googlesource/gerrit/plugins/github/oauth/GitHubLogin.java
+++ b/github-oauth/src/main/java/com/googlesource/gerrit/plugins/github/oauth/GitHubLogin.java
@@ -1,3 +1,17 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// 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.googlesource.gerrit.plugins.github.oauth;
import java.io.IOException;
@@ -15,6 +29,7 @@
public class GitHubLogin {
public AccessToken token;
public GitHub hub;
+ private String redirectUrl;
private transient OAuthProtocol oauth;
@@ -36,8 +51,14 @@
throws IOException {
if (oauth.isOAuthFinal(request)) {
init(oauth.loginPhase2(request, response));
- return isLoggedIn();
+ if(isLoggedIn()) {
+ response.sendRedirect(redirectUrl);
+ return true;
+ } else {
+ return false;
+ }
} else {
+ redirectUrl = request.getRequestURL().toString();
oauth.loginPhase1(request, response);
return false;
}
diff --git a/github-oauth/src/main/java/com/googlesource/gerrit/plugins/github/oauth/OAuthConfig.java b/github-oauth/src/main/java/com/googlesource/gerrit/plugins/github/oauth/OAuthConfig.java
index 5415ca6..faa60a3 100644
--- a/github-oauth/src/main/java/com/googlesource/gerrit/plugins/github/oauth/OAuthConfig.java
+++ b/github-oauth/src/main/java/com/googlesource/gerrit/plugins/github/oauth/OAuthConfig.java
@@ -37,6 +37,8 @@
public final String gitHubClientId;
public final String gitHubClientSecret;
public final String httpHeader;
+ public final String httpDisplaynameHeader;
+ public final String httpEmailHeader;
public final String gitHubOAuthUrl;
public final String oAuthFinalRedirectUrl;
public final String gitHubOAuthAccessTokenUrl;
@@ -46,6 +48,8 @@
public OAuthConfig(@GerritServerConfig Config config)
throws MalformedURLException {
httpHeader = config.getString("auth", null, "httpHeader");
+ httpDisplaynameHeader = config.getString("auth", null, "httpDisplaynameHeader");
+ httpEmailHeader = config.getString("auth", null, "httpEmailHeader");
gitHubUrl =
Objects.firstNonNull(config.getString("github", null, "url"),
GITHUB_URL);
diff --git a/github-oauth/src/main/java/com/googlesource/gerrit/plugins/github/oauth/OAuthCookie.java b/github-oauth/src/main/java/com/googlesource/gerrit/plugins/github/oauth/OAuthCookie.java
index 3bf35d5..d886b82 100644
--- a/github-oauth/src/main/java/com/googlesource/gerrit/plugins/github/oauth/OAuthCookie.java
+++ b/github-oauth/src/main/java/com/googlesource/gerrit/plugins/github/oauth/OAuthCookie.java
@@ -20,10 +20,27 @@
public static final String OAUTH_COOKIE_NAME = "GerritOAuth";
public final String user;
+ public final String email;
+ public final String fullName;
- OAuthCookie(String user, String cookieValue) {
- super(OAUTH_COOKIE_NAME, cookieValue);
-
+ public OAuthCookie(TokenCipher cipher, final String user, final String email,
+ final String fullName) throws OAuthTokenException {
+ super(OAUTH_COOKIE_NAME, cipher.encode(String.format("%s\n%s\n%s", user,
+ email, fullName)));
this.user = user;
+ this.email = email;
+ this.fullName = fullName;
+ setMaxAge((int) (TokenCipher.COOKIE_TIMEOUT/1000L));
+ setHttpOnly(true);
+ }
+
+ public OAuthCookie(TokenCipher cipher, Cookie cookie)
+ throws OAuthTokenException {
+ super(OAUTH_COOKIE_NAME, cookie.getValue());
+ String clearTextValue = cipher.decode(cookie.getValue());
+ String[] clearText = clearTextValue.split("\n");
+ user = clearText.length > 0 ? clearText[0]:null;
+ email = clearText.length > 1 ? clearText[1]:null;
+ fullName = clearText.length > 2 ? clearText[2]:null;
}
}
diff --git a/github-oauth/src/main/java/com/googlesource/gerrit/plugins/github/oauth/OAuthCookieProvider.java b/github-oauth/src/main/java/com/googlesource/gerrit/plugins/github/oauth/OAuthCookieProvider.java
index 2cdaab4..65cd93e 100644
--- a/github-oauth/src/main/java/com/googlesource/gerrit/plugins/github/oauth/OAuthCookieProvider.java
+++ b/github-oauth/src/main/java/com/googlesource/gerrit/plugins/github/oauth/OAuthCookieProvider.java
@@ -26,106 +26,23 @@
import org.slf4j.Logger;
public class OAuthCookieProvider {
- private static final String UTF8 = "UTF-8";
- private static final String ENC_ALGO_PADDING = "AES/CBC/PKCS5Padding";
- private static final String JCE_PROVIDER = "SunJCE";
- private static final String ENC_ALGO = "AES";
- private static final Logger log = org.slf4j.LoggerFactory
- .getLogger(OAuthCookieProvider.class);
- private static final Long COOKIE_TIMEOUT = 15 * 60 * 1000L;
- private SecretKey aesKey;
- private byte[] IV;
- private SecureRandom sessionRnd = new SecureRandom();
+ private TokenCipher cipher;
- void init() {
- KeyGenerator kgen;
- try {
- kgen = KeyGenerator.getInstance(ENC_ALGO);
- kgen.init(128);
- SecureRandom sr = new SecureRandom();
- sr.setSeed(System.currentTimeMillis());
- byte[] key = new byte[16];
- IV = new byte[16];
- sr.nextBytes(key);
- sr.nextBytes(IV);
- aesKey = kgen.generateKey();
- sessionRnd.setSeed(System.currentTimeMillis());
- } catch (NoSuchAlgorithmException e) {
- log.error("Cannot find encryption algorithm " + ENC_ALGO);
- throw new IllegalArgumentException(e);
- }
+ public OAuthCookieProvider(TokenCipher cipher) {
+ this.cipher = cipher;
}
- public OAuthCookie getFromUser(String username) {
+ public OAuthCookie getFromUser(String username, String email, String fullName) {
try {
- return new OAuthCookie(username, encode(username));
+ return new OAuthCookie(cipher, username, email, fullName);
} catch (OAuthTokenException e) {
return null;
}
}
- public OAuthCookie getFromCookie(Cookie cookie) {
- try {
- return new OAuthCookie(decode(cookie.getValue()), cookie.getValue());
- } catch (OAuthTokenException e) {
- return null;
- }
+ public OAuthCookie getFromCookie(Cookie cookie) throws OAuthTokenException {
+ return new OAuthCookie(cipher, cookie);
}
- public String encode(String user) throws OAuthTokenException {
- try {
- long sessionId = sessionRnd.nextLong();
- long ts = System.currentTimeMillis();
- String userSession =
- String.format("%d/%d/%s", sessionId, ts,
- URLEncoder.encode(user, UTF8));
- byte[] plainText =
- (userSession + "/" + userSession.hashCode()).getBytes(UTF8);
-
- Cipher cipher = Cipher.getInstance(ENC_ALGO_PADDING, JCE_PROVIDER);
- cipher.init(Cipher.ENCRYPT_MODE, aesKey, new IvParameterSpec(IV));
- byte[] enc = cipher.doFinal(plainText);
- return org.eclipse.jgit.util.Base64.encodeBytes(enc).trim();
- } catch (Exception e) {
- log.error("Encryption failed", e);
- throw new OAuthTokenException("Cannot generate session token for user "
- + user, e);
- }
- }
-
- public String decode(String sessionToken) throws OAuthTokenException {
- try {
- byte[] enc =
- org.eclipse.jgit.util.Base64.decode(sessionToken.trim().getBytes(),
- 0, sessionToken.length());
- Cipher cipher = Cipher.getInstance(ENC_ALGO_PADDING, JCE_PROVIDER);
- cipher.init(Cipher.DECRYPT_MODE, aesKey, new IvParameterSpec(IV));
-
- String[] clearTextParts =
- new String(cipher.doFinal(enc), UTF8).split("/");
-
- isValid(sessionToken, clearTextParts);
-
- return clearTextParts[2];
- } catch (Exception e) {
- log.error("Decryption failed", e);
- throw new OAuthTokenException("Invalid session token " + sessionToken, e);
- }
- }
-
- private void isValid(String sessionToken, String[] clearTextParts)
- throws OAuthTokenException {
- int hashCode = Integer.parseInt(clearTextParts[3]);
- if (hashCode != (clearTextParts[0] + "/" + clearTextParts[1] + "/" + clearTextParts[2])
- .hashCode()) {
- throw new OAuthTokenException("Invalid or forged token " + sessionToken);
- }
-
- long ts = Long.parseLong(clearTextParts[1]);
- if ((System.currentTimeMillis() - ts) > COOKIE_TIMEOUT) {
- throw new OAuthTokenException("Session token " + sessionToken
- + " has expired");
- }
- }
}
diff --git a/github-oauth/src/main/java/com/googlesource/gerrit/plugins/github/oauth/OAuthFilter.java b/github-oauth/src/main/java/com/googlesource/gerrit/plugins/github/oauth/OAuthFilter.java
index 50cc750..7e43ba7 100644
--- a/github-oauth/src/main/java/com/googlesource/gerrit/plugins/github/oauth/OAuthFilter.java
+++ b/github-oauth/src/main/java/com/googlesource/gerrit/plugins/github/oauth/OAuthFilter.java
@@ -26,15 +26,19 @@
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
+import org.kohsuke.github.GHMyself;
import org.slf4j.LoggerFactory;
import com.google.common.base.Strings;
import com.google.gson.Gson;
import com.google.inject.Inject;
+import com.google.inject.Singleton;
+@Singleton
public class OAuthFilter implements Filter {
private static final org.slf4j.Logger log = LoggerFactory
.getLogger(OAuthFilter.class);
+ private static final String GERRIT_COOKIE_NAME = "GerritAccount";
private final OAuthConfig config;
private final OAuthCookieProvider cookieProvider;
@@ -43,7 +47,7 @@
@Inject
public OAuthFilter(OAuthConfig config) {
this.config = config;
- this.cookieProvider = new OAuthCookieProvider();
+ this.cookieProvider = new OAuthCookieProvider(new TokenCipher());
this.oauth =
new OAuthProtocol(config, GitHubHttpProvider.getInstance().get(),
new Gson());
@@ -51,7 +55,6 @@
@Override
public void init(FilterConfig filterConfig) throws ServletException {
- cookieProvider.init();
}
@Override
@@ -66,18 +69,27 @@
HttpServletResponse httpResponse = (HttpServletResponse) response;
log.info("doFilter(" + httpRequest.getRequestURI() + ")");
- OAuthCookie authCookie = getOAuthCookie(httpRequest);
+ Cookie gerritCookie = getGerritCookie(httpRequest);
+ OAuthCookie authCookie =
+ getOAuthCookie(httpRequest, (HttpServletResponse) response);
String targetUrl = httpRequest.getParameter("state");
- if (authCookie == null) {
+ if (((oauth.isOAuthLogin(httpRequest) || oauth.isOAuthFinal(httpRequest)) && authCookie == null)
+ || (authCookie == null && gerritCookie == null)) {
if (oauth.isOAuthFinal(httpRequest)) {
- String user =
- oauth.loginPhase2(httpRequest, httpResponse).hub.getMyself()
- .getLogin();
+ GHMyself myself =
+ oauth.loginPhase2(httpRequest, httpResponse).hub.getMyself();
+ String user = myself.getLogin();
+ String email = myself.getEmail();
+ String fullName =
+ Strings.emptyToNull(myself.getName()) == null ? user : myself
+ .getName();
if (user != null) {
- httpResponse.addCookie(cookieProvider.getFromUser(user));
+ OAuthCookie userCookie =
+ cookieProvider.getFromUser(user, email, fullName);
+ httpResponse.addCookie(userCookie);
httpResponse.sendRedirect(targetUrl);
return;
} else {
@@ -85,33 +97,63 @@
"Login failed");
}
} else {
- if(oauth.isOAuthLogin(httpRequest)) {
- oauth.loginPhase1(httpRequest, httpResponse);
+ if (oauth.isOAuthLogin(httpRequest)) {
+ oauth.loginPhase1(httpRequest, httpResponse);
} else {
chain.doFilter(request, response);
}
}
return;
} else {
- HttpServletRequest wrappedRequest =
- new AuthenticatedHttpRequest(httpRequest, config.httpHeader,
- authCookie.user);
+ if (gerritCookie != null && !oauth.isOAuthLogin(httpRequest)) {
+ if (authCookie != null) {
+ authCookie.setMaxAge(0);
+ authCookie.setValue("");
+ httpResponse.addCookie(authCookie);
+ }
+ } else if (authCookie != null) {
+ httpRequest =
+ new AuthenticatedHttpRequest(httpRequest, config.httpHeader,
+ authCookie.user, config.httpDisplaynameHeader,
+ authCookie.fullName, config.httpEmailHeader, authCookie.email);
+ }
if (targetUrl != null && oauth.isOAuthFinal(httpRequest)) {
httpResponse.sendRedirect(config.getUrl(targetUrl,
OAuthConfig.OAUTH_FINAL) + "?code=" + request.getParameter("code"));
return;
} else {
- chain.doFilter(wrappedRequest, response);
+ chain.doFilter(httpRequest, response);
}
}
}
- private OAuthCookie getOAuthCookie(HttpServletRequest request) {
+ private Cookie getGerritCookie(HttpServletRequest httpRequest) {
+ for (Cookie cookie : httpRequest.getCookies()) {
+ if (cookie.getName().equalsIgnoreCase(GERRIT_COOKIE_NAME)) {
+ return cookie;
+ }
+ }
+ return null;
+ }
+
+ private OAuthCookie getOAuthCookie(HttpServletRequest request,
+ HttpServletResponse response) {
for (Cookie cookie : request.getCookies()) {
if (cookie.getName().equalsIgnoreCase(OAuthCookie.OAUTH_COOKIE_NAME)
&& !Strings.isNullOrEmpty(cookie.getValue())) {
- return cookieProvider.getFromCookie(cookie);
+ try {
+ return cookieProvider.getFromCookie(cookie);
+ } catch (OAuthTokenException e) {
+ log.warn(
+ "Invalid cookie detected: cleaning up and sending a reset back to the browser",
+ e);
+ cookie.setValue("");
+ cookie.setPath("/");
+ cookie.setMaxAge(0);
+ response.addCookie(cookie);
+ return null;
+ }
}
}
return null;
diff --git a/github-oauth/src/main/java/com/googlesource/gerrit/plugins/github/oauth/OAuthProtocol.java b/github-oauth/src/main/java/com/googlesource/gerrit/plugins/github/oauth/OAuthProtocol.java
index 82af2dd..790df59 100644
--- a/github-oauth/src/main/java/com/googlesource/gerrit/plugins/github/oauth/OAuthProtocol.java
+++ b/github-oauth/src/main/java/com/googlesource/gerrit/plugins/github/oauth/OAuthProtocol.java
@@ -59,7 +59,7 @@
}
public boolean isOAuthLogin(HttpServletRequest request) {
- return request.getRequestURI().endsWith(OAuthConfig.OAUTH_LOGIN);
+ return request.getRequestURI().indexOf(OAuthConfig.OAUTH_LOGIN) >= 0;
}
public GitHubLogin loginPhase2(HttpServletRequest request,
diff --git a/github-oauth/src/main/java/com/googlesource/gerrit/plugins/github/oauth/TokenCipher.java b/github-oauth/src/main/java/com/googlesource/gerrit/plugins/github/oauth/TokenCipher.java
new file mode 100644
index 0000000..9f4cbf6
--- /dev/null
+++ b/github-oauth/src/main/java/com/googlesource/gerrit/plugins/github/oauth/TokenCipher.java
@@ -0,0 +1,119 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// 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.googlesource.gerrit.plugins.github.oauth;
+
+import java.net.URLDecoder;
+import java.net.URLEncoder;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+
+import javax.crypto.Cipher;
+import javax.crypto.KeyGenerator;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.IvParameterSpec;
+
+import org.slf4j.Logger;
+
+import com.google.inject.Singleton;
+
+@Singleton
+public class TokenCipher {
+ private static final String UTF8 = "UTF-8";
+ private static final String ENC_ALGO_PADDING = "AES/CBC/PKCS5Padding";
+ private static final String JCE_PROVIDER = "SunJCE";
+ private static final String ENC_ALGO = "AES";
+ private static final Logger log = org.slf4j.LoggerFactory
+ .getLogger(OAuthCookieProvider.class);
+ public static final Long COOKIE_TIMEOUT = 60 * 1000L;
+
+ private SecretKey aesKey;
+ private byte[] IV;
+ private SecureRandom sessionRnd = new SecureRandom();
+
+ public TokenCipher() {
+ KeyGenerator kgen;
+ try {
+ kgen = KeyGenerator.getInstance(ENC_ALGO);
+ kgen.init(128);
+ SecureRandom sr = new SecureRandom();
+ sr.setSeed(System.currentTimeMillis());
+ byte[] key = new byte[16];
+ IV = new byte[16];
+ sr.nextBytes(key);
+ sr.nextBytes(IV);
+ aesKey = kgen.generateKey();
+ sessionRnd.setSeed(System.currentTimeMillis());
+ } catch (NoSuchAlgorithmException e) {
+ log.error("Cannot find encryption algorithm " + ENC_ALGO);
+ throw new IllegalArgumentException(e);
+ }
+ }
+
+ public String encode(String clearText) throws OAuthTokenException {
+ try {
+ long sessionId = sessionRnd.nextLong();
+ long ts = System.currentTimeMillis();
+ String userSession =
+ String.format("%d/%d/%s", sessionId, ts,
+ URLEncoder.encode(clearText, UTF8));
+ byte[] plainText =
+ (userSession + "/" + userSession.hashCode()).getBytes(UTF8);
+
+ Cipher cipher = Cipher.getInstance(ENC_ALGO_PADDING, JCE_PROVIDER);
+ cipher.init(Cipher.ENCRYPT_MODE, aesKey, new IvParameterSpec(IV));
+ byte[] enc = cipher.doFinal(plainText);
+ return org.eclipse.jgit.util.Base64.encodeBytes(enc).trim();
+ } catch (Exception e) {
+ log.error("Encryption failed", e);
+ throw new OAuthTokenException("Cannot generate token for "
+ + clearText, e);
+ }
+ }
+
+ public String decode(String sessionToken) throws OAuthTokenException {
+ try {
+ byte[] enc =
+ org.eclipse.jgit.util.Base64.decode(sessionToken.trim().getBytes(),
+ 0, sessionToken.length());
+ Cipher cipher = Cipher.getInstance(ENC_ALGO_PADDING, JCE_PROVIDER);
+ cipher.init(Cipher.DECRYPT_MODE, aesKey, new IvParameterSpec(IV));
+
+ String[] clearTextParts =
+ new String(cipher.doFinal(enc), UTF8).split("/");
+
+ isValid(sessionToken, clearTextParts);
+
+ return URLDecoder.decode(clearTextParts[2], UTF8);
+ } catch (Exception e) {
+ log.error("Decryption failed", e);
+ throw new OAuthTokenException("Invalid session token " + sessionToken, e);
+ }
+ }
+
+ private void isValid(String sessionToken, String[] clearTextParts)
+ throws OAuthTokenException {
+ int hashCode = Integer.parseInt(clearTextParts[3]);
+ if (hashCode != (clearTextParts[0] + "/" + clearTextParts[1] + "/" + clearTextParts[2])
+ .hashCode()) {
+ throw new OAuthTokenException("Invalid or forged token " + sessionToken);
+ }
+
+ long ts = Long.parseLong(clearTextParts[1]);
+ if ((System.currentTimeMillis() - ts) > COOKIE_TIMEOUT) {
+ throw new OAuthTokenException("Session token " + sessionToken
+ + " has expired");
+ }
+ }
+}