Skip to content

Commit

Permalink
oAuth
Browse files Browse the repository at this point in the history
* Support PKCE (https://oauth.net/2/pkce/)
* minor refactorings
  • Loading branch information
jrobinso committed Sep 14, 2023
1 parent 4e6ca00 commit 697788f
Show file tree
Hide file tree
Showing 7 changed files with 241 additions and 199 deletions.
3 changes: 1 addition & 2 deletions src/main/java/org/broad/igv/batch/CommandListener.java
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@
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.HttpUtils;
import org.broad.igv.util.StringUtils;

import java.awt.*;
Expand Down Expand Up @@ -228,7 +227,7 @@ private void processClientSession(CommandExecutor cmdExe) throws IOException {
if (params.containsKey("error")) {
sendTextResponse(out, "Error authorizing IGV: " + params.get("error"));
} else if (params.containsKey("code")) {
provider.setAuthorizationCode(params.get("code"));
provider.fetchAccessToken(params.get("code"));
sendTextResponse(out, "Authorization successful. You may close this tab.");
} else if (params.containsKey("token")) {
// Very doubtful this is ever called -- its not a normal OAuth flow
Expand Down
209 changes: 109 additions & 100 deletions src/main/java/org/broad/igv/oauth/OAuthProvider.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@
import java.awt.*;
import java.io.IOException;
import java.net.*;
import java.security.NoSuchAlgorithmException;
import java.time.Duration;
import java.util.*;
import java.util.prefs.Preferences;

public class OAuthProvider {

Expand All @@ -33,7 +33,6 @@ public class OAuthProvider {
private String appIdURI;
public static String findString = null;
public static String replaceString = null;
private static final String REFRESH_TOKEN_KEY = "oauth_refresh_token";
private String state; // "RFC6749: An opaque value used by the client to maintain state"
private String redirectURI;
private String clientId;
Expand All @@ -42,6 +41,9 @@ public class OAuthProvider {
private String tokenEndpoint;
private String accessToken;
private String refreshToken;
private String codeChallenge;
private String codeVerifier;
private String codeChallengeMethod;
private long expirationTime; // in milliseconds
private String scope;
private String[] hosts;
Expand All @@ -61,39 +63,39 @@ public OAuthProvider(JsonObject obj) throws IOException {
}

// Mandatory attributes, fail hard if not present
try {
clientId = obj.get("client_id").getAsString();
authEndpoint = obj.has("auth_uri") ?
obj.get("auth_uri").getAsString() :
obj.get("authorization_endpoint").getAsString();
tokenEndpoint = obj.has("token_uri") ?
obj.get("token_uri").getAsString() :
obj.get("token_endpoint").getAsString();

} catch (Exception e) {
log.error(e);
throw new IOException("oauthConfig is missing crucial attributes such as: client_id, " +
"authorization_endpoint/auth_uri or token_endpoint/token_uri");
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"))) {
throw new RuntimeException("oauthConfig is missing crucial attributes such as: client_id, client_secret, " +
"authorization_endpoint/auth_uri, or token_endpoint/token_uri.");
}

String portNumber = PreferencesManager.getPreferences().getPortNumber();
redirectURI = "http://localhost:" + portNumber + "/oauthCallback";;

// Optional or custom attributes, fail on runtime, depending on identity provider configuration
clientId = obj.get("client_id").getAsString();
authEndpoint = obj.has("auth_uri") ?
obj.get("auth_uri").getAsString() :
obj.get("authorization_endpoint").getAsString();
tokenEndpoint = obj.has("token_uri") ?
obj.get("token_uri").getAsString() :
obj.get("token_endpoint").getAsString();
clientSecret = obj.has("client_secret") ? obj.get("client_secret").getAsString() : null;
setAuthProvider(obj.has("auth_provider") ? obj.get("auth_provider").getAsString() : authProvider);

// app ID URI is a Microsoft
appIdURI = obj.has("app_id_uri") ?
obj.get("app_id_uri").getAsString() :
obj.has("resource") ? obj.get("resource").getAsString() : null;


findString = obj.has("find_string") ? obj.get("find_string").getAsString() : null;
replaceString = obj.has("replace_string") ? obj.get("replace_string").getAsString() : null;
// Optional attributes
if (obj.has("auth_provider")) {
authProvider = obj.get("auth_provider").getAsString();
}
if (obj.has("scope")) {
scope = obj.get("scope").getAsString();
} else if (isGoogle()) {
String gsScope = "https://www.googleapis.com/auth/devstorage.read_only";
String emailScope = "https://www.googleapis.com/auth/userinfo.email";
scope = gsScope + "%20" + emailScope;

}
appIdURI = obj.has("app_id_uri") ? // Microsoft Azure.
obj.get("app_id_uri").getAsString() :
obj.has("resource") ? obj.get("resource").getAsString() : null;

if (obj.has("hosts")) {
// hosts element may be an array or a single string - put in hosts array either way
JsonElement hostsElement = obj.get("hosts");
Expand All @@ -108,14 +110,12 @@ public OAuthProvider(JsonObject obj) throws IOException {
}
}

// Special Google properties
if (isGoogle()) {
if (scope == null) {
String gsScope = "https://www.googleapis.com/auth/devstorage.read_only";
String emailScope = "https://www.googleapis.com/auth/userinfo.email";
scope = gsScope + "%20" + emailScope;
}
}
String portNumber = PreferencesManager.getPreferences().getPortNumber();
redirectURI = "http://localhost:" + portNumber + "/oauthCallback";

// Deprecated properties -- for backward compatibility
findString = obj.has("find_string") ? obj.get("find_string").getAsString() : null;
replaceString = obj.has("replace_string") ? obj.get("replace_string").getAsString() : null;
}

public String getState() {
Expand All @@ -134,26 +134,38 @@ public String getState() {
*/
public void openAuthorizationPage() throws IOException, URISyntaxException {

String url;

// If the port listener is not on, try starting it
if (!CommandListener.isListening()) {
CommandListener.start();
}

// if the listener is not active, prompt the user
// for the access token
// if the listener is not active, prompt the user for the access token
if (!CommandListener.isListening()) {
String ac = MessageUtils.showInputDialog("The IGV port listener is required for OAuth authentication. If you have an access token enter it here.");
String ac = MessageUtils.showInputDialog(
"The IGV port listener is off and is required for OAuth authentication through IGV<br/>. " +
"If you have an access token obtained by other means enter it here.");
if (ac != null) {
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);
}

url = authEndpoint + "?state=" + state +
String url = authEndpoint +
"?state=" + state +
"&redirect_uri=" + URLEncoder.encode(redirectURI, "utf-8") +
"&client_id=" + clientId +
"&response_type=code";
"&response_type=code" +
"&code_challenge=" + codeChallenge +
"&code_challenge_method=" + codeChallengeMethod;

if (scope != null) {
url += "&scope=" + scope;
Expand All @@ -174,7 +186,7 @@ public void openAuthorizationPage() throws IOException, URISyntaxException {
}

// Called from port listener (org.broad.igv.batch.CommandListener) upon receiving the oauth request with a "code" parameter
public void setAuthorizationCode(String authorizationCode) throws IOException {
public void fetchAccessToken(String authorizationCode) throws IOException {

URL url = HttpUtils.createURL(tokenEndpoint);

Expand All @@ -184,10 +196,13 @@ public void setAuthorizationCode(String authorizationCode) throws IOException {
if (clientSecret != null) {
params.put("client_secret", clientSecret);
}
params.put("state", state);
params.put("redirect_uri", redirectURI);
params.put("grant_type", "authorization_code");
params.put("code_verifier", codeVerifier);


// set the resource if it necessary for the auth provider dwm08
// set the resource if necessary for the auth provider
if (appIdURI != null) {
params.put("resource", appIdURI);
}
Expand All @@ -197,11 +212,10 @@ public void setAuthorizationCode(String authorizationCode) throws IOException {
String res = HttpUtils.getInstance().doPost(url, params);
JsonParser parser = new JsonParser();

setResponse(parser.parse(res).getAsJsonObject());

response = parser.parse(res).getAsJsonObject();
accessToken = response.get("access_token").getAsString();
refreshToken = response.get("refresh_token").getAsString();
expirationTime = System.currentTimeMillis() + (response.get("expires_in").getAsInt() * 1000);
expirationTime = System.currentTimeMillis() + response.get("expires_in").getAsInt() * 1000;

// Populate this class with user profile attributes
if (response.has("id_token")) {
Expand Down Expand Up @@ -241,13 +255,14 @@ public void setAccessToken(String accessToken) {
*/
private void refreshAccessToken() throws IOException {

// properties moved to early init dwm08
//if (clientId == null) fetchOauthProperties();
log.debug("Refresh access token");

Map<String, String> params = new HashMap<String, String>();
params.put("refresh_token", refreshToken);
params.put("client_id", clientId);
params.put("client_secret", clientSecret);
if (clientSecret != null) {
params.put("client_secret", clientSecret);
}
params.put("grant_type", "refresh_token");

// set the resource if it necessary for the auth provider dwm08
Expand All @@ -258,16 +273,17 @@ private void refreshAccessToken() throws IOException {
// Poke the token refresh endpoint to get new access key
URL url = HttpUtils.createURL(tokenEndpoint);

String response = HttpUtils.getInstance().doPost(url, params);
String responseString = HttpUtils.getInstance().doPost(url, params);
JsonParser parser = new JsonParser();
response = parser.parse(responseString).getAsJsonObject();

setResponse(parser.parse(response).getAsJsonObject());
JsonObject obj = getResponse();

JsonPrimitive atprim = obj.getAsJsonPrimitive("access_token");
JsonPrimitive atprim = response.getAsJsonPrimitive("access_token");
if (atprim != null) {
accessToken = obj.getAsJsonPrimitive("access_token").getAsString();
expirationTime = System.currentTimeMillis() + (obj.getAsJsonPrimitive("expires_in").getAsInt() * 1000);
accessToken = response.getAsJsonPrimitive("access_token").getAsString();
if (response.has("refresh_token")) {
refreshToken = response.getAsJsonPrimitive("refresh_token").getAsString();
}
expirationTime = System.currentTimeMillis() + response.getAsJsonPrimitive("expires_in").getAsInt() * 1000;
} else {
// Refresh token has failed, reauthorize from scratch
reauthorize();
Expand Down Expand Up @@ -331,36 +347,6 @@ public Duration getExpirationTime() {
return expiration;
}

public class AuthStateEvent {
boolean authenticated;
String authProvider;
String userName;
String email;

// Assuming that if this event is called, we are indeed autz/authn'd
public AuthStateEvent(boolean authenticated, String authProvider, String userName) {
this.authenticated = authenticated;
this.authProvider = authProvider;
this.userName = userName;
}

public boolean isAuthenticated() {
return authenticated;
}

public String getAuthProvider() {
return authProvider;
}

public String getUserName() {
return userName;
}

public String getEmail() {
return currentUserEmail;
}
}

public boolean isLoggedIn() {
return accessToken != null;
}
Expand All @@ -369,22 +355,18 @@ public String getCurrentUserName() {
return currentUserName != null ? currentUserName : (currentUserEmail != null ? currentUserEmail : currentUserID);
}

public String getCurrentUserEmail() {
return currentUserEmail;
}

public void logout() {
accessToken = null;
refreshToken = null;
expirationTime = -1;
currentUserName = null;
removeRefreshToken();
IGVEventBus.getInstance().post(new AuthStateEvent(false, this.authProvider, null));
}

private void removeRefreshToken() {
try {
Preferences.userRoot().remove(REFRESH_TOKEN_KEY);
} catch (Exception e) {
log.error("Error removing oauth refresh token", e);
}
}

/**
* If not logged in, attempt to login
Expand Down Expand Up @@ -443,12 +425,39 @@ public JsonObject getResponse() {
return response;
}

public void setResponse(JsonObject res) {
response = res;
}

public void setAuthProvider(String authProvider) {
this.authProvider = authProvider;
}

public String getAuthProvider() {
return authProvider;
}

public static class AuthStateEvent {
boolean authenticated;
String authProvider;
String userName;

// Assuming that if this event is called, we are indeed autz/authn'd
public AuthStateEvent(boolean authenticated, String authProvider, String userName) {
this.authenticated = authenticated;
this.authProvider = authProvider;
this.userName = userName;
}

public boolean isAuthenticated() {
return authenticated;
}

public String getAuthProvider() {
return authProvider;
}

public String getUserName() {
return userName;
}
}
}



Loading

0 comments on commit 697788f

Please sign in to comment.