diff --git a/examples/oauth/oauth-config-amazon.json b/examples/oauth/oauth-config-amazon.json index 410cd9022c..74ee4d2327 100644 --- a/examples/oauth/oauth-config-amazon.json +++ b/examples/oauth/oauth-config-amazon.json @@ -3,13 +3,13 @@ "project_id": "igv", "auth_provider": "Amazon", "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", - "aws_region": "ap-southeast-2", + "aws_region": "us-east-1", "scope": "email%20openid%20profile", - "client_id": "3f4ujenfmr77tg12iofbebpkoh", - "client_secret": "en1q6638m4dogrr6erhosetim67sjilc6htjnfmf6ljk2q3j9og", - "authorization_endpoint": "https://igv-demo.auth.ap-southeast-2.amazoncognito.com/login", - "token_endpoint": "https://igv-demo.auth.ap-southeast-2.amazoncognito.com/token", - "aws_cognito_fed_pool_id": "ap-southeast-2:15b7bf93-18ca-40d5-99e9-38b4eb69363e", - "aws_cognito_pool_id": "ap-southeast-2_IYMvlZzmv", - "aws_cognito_role_arn": "arn:aws:iam::YOUR_AWS_ACCOUNT:role/YOUR_Cognito_igvAuth_Role" + "client_id": "eu7vd09a3dsfasdfd7r4kjomqg", + "client_secret": "s2416djkq5liigbrjdhktdsdgerefktsv04f9p810a3nc9", + "authorization_endpoint": "https://igv-demo.auth.us-east-1.amazoncognito.com/login", + "token_endpoint": "https://igv-demo.auth.us-east-1.amazoncognito.com/token", + "aws_cognito_fed_pool_id": "us-east-1:2926325e-0938-4dfc-840a-fa8a3e4f63f7", + "aws_cognito_pool_id": "us-east-1_v98erojsdkfj", + "aws_cognito_role_arn": "arn:aws:iam::3940820579:role/Cognito_igvAuth_Role" } \ No newline at end of file diff --git a/src/main/java/org/broad/igv/Globals.java b/src/main/java/org/broad/igv/Globals.java index 502eebaf88..0cf1b5a089 100644 --- a/src/main/java/org/broad/igv/Globals.java +++ b/src/main/java/org/broad/igv/Globals.java @@ -55,7 +55,7 @@ public class Globals { private static boolean testing = false; public static int CONNECT_TIMEOUT = 20000; // 20 seconds public static int READ_TIMEOUT = 1000 * 3 * 60; // 3 minutes - public static int TOKEN_EXPIRE_GRACE_TIME = 1000 * 1 * 60; // 1 minute + public static int TOKEN_EXPIRE_GRACE_TIME = 1000 * 60; // 1 minute /** * Field description diff --git a/src/main/java/org/broad/igv/batch/CommandListener.java b/src/main/java/org/broad/igv/batch/CommandListener.java index 6cad61678a..95a6ca76de 100755 --- a/src/main/java/org/broad/igv/batch/CommandListener.java +++ b/src/main/java/org/broad/igv/batch/CommandListener.java @@ -222,7 +222,6 @@ private void processClientSession(CommandExecutor cmdExe) throws IOException { // Detect oauth callback if (command.equals("/oauthCallback")) { - OAuthProvider provider = OAuthUtils.getInstance().getProviderForState(params.get("state")); if (params.containsKey("error")) { sendTextResponse(out, "Error authorizing IGV: " + params.get("error")); @@ -237,12 +236,11 @@ private void processClientSession(CommandExecutor cmdExe) throws IOException { } else { sendTextResponse(out, "Unsuccessful authorization response: " + inputLine); } - - if (PreferencesManager.getPreferences().getAsBoolean(Constants.PORT_ENABLED) == false) { // Turn off port halt(); } + } else { // Process the request. result = processGet(command, params, cmdExe); // Send no response if result is "OK". @@ -312,7 +310,7 @@ private void closeSockets() { private static final String NO_CACHE = "Cache-Control: no-cache, no-store"; private static final String ACCESS_CONTROL_ALLOW_ORIGIN = "Access-Control-Allow-Origin: *"; private static final String ACCESS_CONTROL_ALLOW_HEADERS = "Access-Control-Allow-Headers: access-control-allow-origin"; - + private void sendTextResponse(PrintWriter out, String result) { sendHTTPResponse(out, result, "text/html", "GET"); } diff --git a/src/main/java/org/broad/igv/oauth/OAuthProvider.java b/src/main/java/org/broad/igv/oauth/OAuthProvider.java index 92105ad1c0..f352bd6781 100644 --- a/src/main/java/org/broad/igv/oauth/OAuthProvider.java +++ b/src/main/java/org/broad/igv/oauth/OAuthProvider.java @@ -29,6 +29,8 @@ public class OAuthProvider { private static Logger log = LogManager.getLogger(OAuthProvider.class); + private static int TOKEN_EXPIRE_GRACE_TIME = 1000 * 60; // 1 minute + private String authProvider = ""; private String appIdURI; public static String findString = null; @@ -65,8 +67,7 @@ public OAuthProvider(JsonObject obj) throws IOException { // Mandatory attributes, fail hard if not present if (!(obj.has("client_id") && (obj.has("auth_uri") || obj.has("authorization_endpoint")) && - (obj.has("token_uri") || obj.has("token_endpoint")) && - obj.has("client_secret"))) { + (obj.has("token_uri") || obj.has("token_endpoint")))) { throw new RuntimeException("oauthConfig is missing crucial attributes such as: client_id, client_secret, " + "authorization_endpoint/auth_uri, or token_endpoint/token_uri."); } @@ -110,8 +111,26 @@ public OAuthProvider(JsonObject obj) throws IOException { } } - String portNumber = PreferencesManager.getPreferences().getPortNumber(); - redirectURI = "http://localhost:" + portNumber + "/oauthCallback"; + if (obj.has("redirect_uris")) { + JsonArray urisArray = obj.get("redirect_uris").getAsJsonArray(); + redirectURI = urisArray.get(0).getAsString(); + } else if (obj.has("redirect_uri")) { + redirectURI = obj.get("redirect_uri").getAsString(); + } else { + String portNumber = PreferencesManager.getPreferences().getPortNumber(); + redirectURI = "http://localhost:" + portNumber + "/oauthCallback"; + } + + // Generate PKCE challenge and verifier + codeVerifier = PKCEUtils.generateCodeVerifier(); + try { + codeChallenge = PKCEUtils.generateCodeChallange(codeVerifier); + codeChallengeMethod = "S256"; + } catch (Exception e) { + codeChallenge = codeVerifier; + codeChallengeMethod = "plain"; + log.error("Error encoding PKCE challenge", e); + } // Deprecated properties -- for backward compatibility findString = obj.has("find_string") ? obj.get("find_string").getAsString() : null; @@ -148,16 +167,6 @@ public void openAuthorizationPage() throws IOException, URISyntaxException { setAccessToken(ac); } } else { - // Generate PKCE challenge and verifier - codeVerifier = PKCEUtils.generateCodeVerifier(); - try { - codeChallenge = PKCEUtils.generateCodeChallange(codeVerifier); - codeChallengeMethod = "S256"; - } catch (NoSuchAlgorithmException e) { - codeChallenge = codeVerifier; - codeChallengeMethod = "plain"; - log.error("Error encoding PKCE challenge", e); - } String url = authEndpoint + "?state=" + state + @@ -201,7 +210,6 @@ public void fetchAccessToken(String authorizationCode) throws IOException { params.put("grant_type", "authorization_code"); params.put("code_verifier", codeVerifier); - // set the resource if necessary for the auth provider if (appIdURI != null) { params.put("resource", appIdURI); @@ -324,7 +332,7 @@ public JsonObject fetchUserProfile(JsonObject jwt_payload) { public String getAccessToken() { // Check expiration time, with 1 minute cushion - if (accessToken == null || (System.currentTimeMillis() > (expirationTime - Globals.TOKEN_EXPIRE_GRACE_TIME))) { + if (accessToken == null || (System.currentTimeMillis() > (expirationTime - TOKEN_EXPIRE_GRACE_TIME))) { log.debug("Refreshing access token!"); if (refreshToken != null) { try { diff --git a/src/main/java/org/broad/igv/oauth/OAuthURLForm.java b/src/main/java/org/broad/igv/oauth/OAuthURLForm.java index 494e85b99e..4b8e9d8f6e 100644 --- a/src/main/java/org/broad/igv/oauth/OAuthURLForm.java +++ b/src/main/java/org/broad/igv/oauth/OAuthURLForm.java @@ -50,11 +50,6 @@ public static void open(Frame owner, String url) { } - - public static void main(String[] args) { - - open(null, "https://docs.oracle.com/javase/tutorial/displayCode.html?code=https://docs.oracle.com/javase/tutorial/uiswing/examples/layout/BoxLayoutDemoProject/src/layout/BoxLayoutDemo.java"); - } } diff --git a/src/main/java/org/broad/igv/oauth/OAuthUtils.java b/src/main/java/org/broad/igv/oauth/OAuthUtils.java index 3463a058b9..8e67c11f20 100644 --- a/src/main/java/org/broad/igv/oauth/OAuthUtils.java +++ b/src/main/java/org/broad/igv/oauth/OAuthUtils.java @@ -73,39 +73,17 @@ public static synchronized OAuthUtils getInstance() { private OAuthUtils() { try { providerCache = new LinkedHashMap<>(); // Ordered (linked) map is important - fetchOauthProperties(); + fetchOauthConfigs(); } catch (Exception e) { log.error("Error fetching oAuth properties", e); } } /** - * Called by AWS code only + * Fetch user-configured oAuth configurations, if any* + * @throws IOException */ - public OAuthProvider getAWSProvider() { - if (awsProvider == null) { - throw new RuntimeException("AWS Oauth is not configured"); - } - return awsProvider; - } - - public OAuthProvider getGoogleProvider() { - if (googleProvider == null) { - try { - log.info("Loading Google oAuth properties"); - googleProvider = loadDefaultOauthProperties(); - if (IGVMenuBar.getInstance() != null) { - IGVMenuBar.getInstance().enableGoogleMenu(true); - } - } catch (IOException e) { - log.error("Error loading Google oAuth properties", e); - MessageUtils.showErrorMessage("Error loading Google oAuth properties", e); - } - } - return googleProvider; - } - - private void fetchOauthProperties() throws IOException { + private void fetchOauthConfigs() throws IOException { // Load a provider config specified in preferences String provisioningURL = PreferencesManager.getPreferences().getProvisioningURL(); @@ -131,12 +109,39 @@ private void fetchOauthProperties() throws IOException { } } + /** + * Called by AWS code only + */ + public OAuthProvider getAWSProvider() { + if (awsProvider == null) { + throw new RuntimeException("AWS Oauth is not configured"); + } + return awsProvider; + } + + public OAuthProvider getGoogleProvider() { + if (googleProvider == null) { + try { + log.info("Loading Google oAuth properties"); + googleProvider = loadDefaultOauthProperties(); + if (IGVMenuBar.getInstance() != null) { + IGVMenuBar.getInstance().enableGoogleMenu(true); + } + } catch (IOException e) { + log.error("Error loading Google oAuth properties", e); + MessageUtils.showErrorMessage("Error loading Google oAuth properties", e); + } + } + return googleProvider; + } + + /** * Load the default (Google) oAuth properties * * @throws IOException */ - public OAuthProvider loadDefaultOauthProperties() throws IOException { + private OAuthProvider loadDefaultOauthProperties() throws IOException { String json = loadAsString(PROPERTIES_URL); JsonParser parser = new JsonParser(); JsonObject obj = parser.parse(json).getAsJsonObject(); diff --git a/src/main/java/org/broad/igv/oauth/OauthListener.java b/src/main/java/org/broad/igv/oauth/OauthListener.java new file mode 100755 index 0000000000..52d05993ae --- /dev/null +++ b/src/main/java/org/broad/igv/oauth/OauthListener.java @@ -0,0 +1,208 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2007-2015 Broad Institute + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package org.broad.igv.oauth; + +import biz.source_code.base64Coder.Base64Coder; +import org.broad.igv.Globals; +import org.broad.igv.batch.CommandExecutor; +import org.broad.igv.feature.genome.GenomeManager; +import org.broad.igv.logging.LogManager; +import org.broad.igv.logging.Logger; +import org.broad.igv.prefs.Constants; +import org.broad.igv.prefs.PreferencesManager; +import org.broad.igv.ui.IGV; +import org.broad.igv.ui.util.MessageUtils; +import org.broad.igv.ui.util.UIUtilities; +import org.broad.igv.util.StringUtils; + +import java.awt.*; +import java.io.*; +import java.net.ServerSocket; +import java.net.Socket; +import java.net.URLDecoder; +import java.nio.channels.ClosedByInterruptException; +import java.security.NoSuchAlgorithmException; +import java.util.*; + +/** + * NOT CURRENTLY USED -- developed for dynamic redirectURIs (dynamic random port numers), possible with Google but not Amazaon + */ + +public class OauthListener implements Runnable { + + private static Logger log = LogManager.getLogger(OauthListener.class); + + private static final String CRLF = "\r\n"; + + private int port; + private OAuthProvider provider; + private Thread listenerThread; + + + public static synchronized void start(int port, OAuthProvider provider) { + OauthListener listener = null; + try { + listener = new OauthListener(port, provider); + listener.listenerThread.start(); + } catch (Exception e) { + log.error(e); + } + } + + private OauthListener(int port, OAuthProvider provider) { + this.port = port; + this.provider = provider; + listenerThread = new Thread(this); + } + + public void run() { + + try (ServerSocket serverSocket = new ServerSocket(port); + Socket clientSocket = serverSocket.accept();) { + + PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true); + BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream())); + + String inputLine = in.readLine(); + System.out.println(inputLine); + + // Consume the remainder of the request, if any (typically request headers). This is important to free the connection. + String nextLine = in.readLine(); + while (nextLine != null && nextLine.length() > 0) { + nextLine = in.readLine(); + System.out.println(nextLine); + } + + String[] tokens = inputLine.split(" "); + if (tokens.length < 2) { + sendTextResponse(out, "ERROR unexpected oauth request: " + inputLine); + } else { + + String[] parts = tokens[1].split("\\?"); + Map params = parts.length < 2 ? new HashMap() : parseParameters(parts[1]); + + if (params.containsKey("error")) { + sendTextResponse(out, "Error authorizing IGV: " + params.get("error")); + } else if (params.containsKey("code")) { + provider.fetchAccessToken(params.get("code")); + sendTextResponse(out, "Authorization successful. You may close this tab."); + } else { + sendTextResponse(out, "Unsuccessful authorization response: " + inputLine); + } + } + } catch (java.net.BindException e) { + MessageUtils.showErrorMessage("Error opening listener for oAuth authorization", e); + } catch (Exception e) { + MessageUtils.showErrorMessage("Error opening listener for oAuth authorization", e); + log.error(e); + } + } + + + private static final String HTTP_RESPONSE = "HTTP/1.1 200 OK"; + private static final String HTTP_NO_RESPONSE = "HTTP/1.1 204 No Response"; + private static final String CONNECTION_CLOSE = "Connection: close"; + private static final String NO_CACHE = "Cache-Control: no-cache, no-store"; + private static final String ACCESS_CONTROL_ALLOW_ORIGIN = "Access-Control-Allow-Origin: *"; + + private void sendTextResponse(PrintWriter out, String result) { + sendHTTPResponse(out, result, "text/html", "GET"); + } + + private void sendHTTPResponse(PrintWriter out, String result, String contentType, String method) { + + out.print(result == null ? HTTP_NO_RESPONSE : HTTP_RESPONSE); + out.print(CRLF); + out.print(ACCESS_CONTROL_ALLOW_ORIGIN); + out.print(CRLF); + if (result != null) { + out.print("Content-Type: " + contentType); + out.print(CRLF); + out.print("Content-Length: " + (result.length())); + out.print(CRLF); + out.print(NO_CACHE); + out.print(CRLF); + out.print(CONNECTION_CLOSE); + out.print(CRLF); + + if (!method.equals("HEAD")) { + out.print(CRLF); + out.print(result); + out.print(CRLF); + } + } else { + out.print(CRLF); + } + out.close(); + } + + + /** + * Parse the html parameter string into a set of key-value pairs. Parameter values are + * url decoded with the exception of the "locus" parameter. + * + * @param parameterString + * @return + */ + private Map parseParameters(String parameterString) { + + // Do a partial decoding now (ampersands only) + parameterString = parameterString.replace("&", "&"); + + HashMap params = new HashMap(); + String[] kvPairs = parameterString.split("&"); + for (String kvString : kvPairs) { + // Split on the first "=", all others are part of the parameter value + String[] kv = kvString.split("=", 2); + if (kv.length == 1) { + params.put(kv[0], null); + } else { + String key = StringUtils.decodeURL(kv[0]); + String value = StringUtils.decodeURL(kv[1]); + params.put(key, value); + } + } + return params; + } + + /** + * Find and available port* + * @return + */ + public static int findFreePort() { + for (int port = 49152; port < 65536; port++) { + try (ServerSocket serverSocket = new ServerSocket(port)) { + if (serverSocket != null && serverSocket.getLocalPort() == port) { + return port; + } + } catch (IOException e) { + //Port is not available, try again + } + } + return -1; + } + +} diff --git a/src/main/java/org/broad/igv/util/AmazonUtils.java b/src/main/java/org/broad/igv/util/AmazonUtils.java index 7378596c02..2df905a817 100644 --- a/src/main/java/org/broad/igv/util/AmazonUtils.java +++ b/src/main/java/org/broad/igv/util/AmazonUtils.java @@ -53,6 +53,9 @@ public class AmazonUtils { private static Credentials cognitoAWSCredentials = null; + private static int TOKEN_EXPIRE_GRACE_TIME = 1000 * 60; // 1 minute + + /** * Maps s3:// URLs to presigned URLs @@ -576,7 +579,7 @@ private static boolean isPresignedURLValid(URL url) { try { long presignedTime = signedURLValidity(url); - isValidSignedUrl = presignedTime - System.currentTimeMillis() - Globals.TOKEN_EXPIRE_GRACE_TIME > 0; // Duration in milliseconds + isValidSignedUrl = presignedTime - System.currentTimeMillis() - TOKEN_EXPIRE_GRACE_TIME > 0; // Duration in milliseconds } catch (ParseException e) { log.error("The AWS signed URL date parameter X-Amz-Date has incorrect formatting"); isValidSignedUrl = false;