Unverified Commit 8e00f6e0 authored by Greg Messner's avatar Greg Messner Committed by GitHub
Browse files

Use secure passwords for OAuth2 logins (#157)

parent 00d2e911
......@@ -7,6 +7,7 @@ import javax.ws.rs.NotAuthorizedException;
import javax.ws.rs.core.Form;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.StreamingOutput;
import org.gitlab4j.api.GitLabApi.ApiVersion;
......@@ -141,6 +142,25 @@ public abstract class AbstractApi implements Constants {
}
}
/**
* Perform an HTTP POST call with the specified payload object and path objects, returning
* a ClientResponse instance with the data returned from the endpoint.
*
* @param expectedStatus the HTTP status that should be returned from the server
* @param stream the StreamingOutput taht will be used for the POST data
* @param mediaType the content-type for the streamed data
* @param pathArgs variable list of arguments used to build the URI
* @return a ClientResponse instance with the data returned from the endpoint
* @throws GitLabApiException if any exception occurs during execution
*/
protected Response post(Response.Status expectedStatus, StreamingOutput stream, String mediaType, Object... pathArgs) throws GitLabApiException {
try {
return validate(getApiClient().post(stream, mediaType, pathArgs), expectedStatus);
} catch (Exception e) {
throw handle(e);
}
}
/**
* Perform an HTTP POST call with the specified form data and path objects, returning
* a ClientResponse instance with the data returned from the endpoint.
......
......@@ -5,6 +5,7 @@ import java.util.Map;
import java.util.Optional;
import java.util.WeakHashMap;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import org.gitlab4j.api.Constants.TokenType;
......@@ -12,6 +13,8 @@ import org.gitlab4j.api.models.OauthTokenResponse;
import org.gitlab4j.api.models.Session;
import org.gitlab4j.api.models.User;
import org.gitlab4j.api.models.Version;
import org.gitlab4j.api.utils.Oauth2LoginStreamingOutput;
import org.gitlab4j.api.utils.SecretString;
/**
* This class is provides a simplified interface to a GitLab API server, and divides the API up into
......@@ -95,11 +98,44 @@ public class GitLabApi {
* @param password password for a given {@code username}
* @return new {@code GitLabApi} instance configured for a user-specific token
* @throws GitLabApiException GitLabApiException if any exception occurs during execution
* @deprecated As of release 4.8.7, replaced by {@link #oauth2Login(String, String, CharSequence)}, will be removed in 4.9.0
*/
@Deprecated
public static GitLabApi oauth2Login(String url, String username, String password) throws GitLabApiException {
return (GitLabApi.oauth2Login(ApiVersion.V4, url, username, password, null, null, false));
}
/**
* <p>Logs into GitLab using OAuth2 with the provided {@code username} and {@code password},
* and creates a new {@code GitLabApi} instance using returned access token.</p>
*
* @param url GitLab URL
* @param username user name for which private token should be obtained
* @param password a CharSequence containing the password for a given {@code username}
* @return new {@code GitLabApi} instance configured for a user-specific token
* @throws GitLabApiException GitLabApiException if any exception occurs during execution
*/
public static GitLabApi oauth2Login(String url, String username, CharSequence password) throws GitLabApiException {
return (GitLabApi.oauth2Login(ApiVersion.V4, url, username, password, null, null, false));
}
/**
* <p>Logs into GitLab using OAuth2 with the provided {@code username} and {@code password},
* and creates a new {@code GitLabApi} instance using returned access token.</p>
*
* @param url GitLab URL
* @param username user name for which private token should be obtained
* @param password a char array holding the password for a given {@code username}
* @return new {@code GitLabApi} instance configured for a user-specific token
* @throws GitLabApiException GitLabApiException if any exception occurs during execution
*/
public static GitLabApi oauth2Login(String url, String username, char[] password) throws GitLabApiException {
try (SecretString secretPassword = new SecretString(password)) {
return (GitLabApi.oauth2Login(ApiVersion.V4, url, username, secretPassword, null, null, false));
}
}
/**
* <p>Logs into GitLab using OAuth2 with the provided {@code username} and {@code password},
* and creates a new {@code GitLabApi} instance using returned access token.</p>
......@@ -110,11 +146,46 @@ public class GitLabApi {
* @param ignoreCertificateErrors if true will set up the Jersey system ignore SSL certificate errors
* @return new {@code GitLabApi} instance configured for a user-specific token
* @throws GitLabApiException GitLabApiException if any exception occurs during execution
* @deprecated As of release 4.8.7, replaced by {@link #oauth2Login(String, String, CharSequence, boolean)}, will be removed in 4.9.0
*/
@Deprecated
public static GitLabApi oauth2Login(String url, String username, String password, boolean ignoreCertificateErrors) throws GitLabApiException {
return (GitLabApi.oauth2Login(ApiVersion.V4, url, username, password, null, null, ignoreCertificateErrors));
}
/**
* <p>Logs into GitLab using OAuth2 with the provided {@code username} and {@code password},
* and creates a new {@code GitLabApi} instance using returned access token.</p>
*
* @param url GitLab URL
* @param username user name for which private token should be obtained
* @param password a CharSequence containing the password for a given {@code username}
* @param ignoreCertificateErrors if true will set up the Jersey system ignore SSL certificate errors
* @return new {@code GitLabApi} instance configured for a user-specific token
* @throws GitLabApiException GitLabApiException if any exception occurs during execution
*/
public static GitLabApi oauth2Login(String url, String username, CharSequence password, boolean ignoreCertificateErrors) throws GitLabApiException {
return (GitLabApi.oauth2Login(ApiVersion.V4, url, username, password, null, null, ignoreCertificateErrors));
}
/**
* <p>Logs into GitLab using OAuth2 with the provided {@code username} and {@code password},
* and creates a new {@code GitLabApi} instance using returned access token.</p>
*
* @param url GitLab URL
* @param username user name for which private token should be obtained
* @param password a char array holding the password for a given {@code username}
* @param ignoreCertificateErrors if true will set up the Jersey system ignore SSL certificate errors
* @return new {@code GitLabApi} instance configured for a user-specific token
* @throws GitLabApiException GitLabApiException if any exception occurs during execution
*/
public static GitLabApi oauth2Login(String url, String username, char[] password, boolean ignoreCertificateErrors) throws GitLabApiException {
try (SecretString secretPassword = new SecretString(password)) {
return (GitLabApi.oauth2Login(ApiVersion.V4, url, username, secretPassword, null, null, ignoreCertificateErrors));
}
}
/**
* <p>Logs into GitLab using OAuth2 with the provided {@code username} and {@code password},
* and creates a new {@code GitLabApi} instance using returned access token.</p>
......@@ -127,13 +198,77 @@ public class GitLabApi {
* @param ignoreCertificateErrors if true will set up the Jersey system ignore SSL certificate errors
* @return new {@code GitLabApi} instance configured for a user-specific token
* @throws GitLabApiException GitLabApiException if any exception occurs during execution
* @deprecated As of release 4.8.7, will be removed in 4.9.0
*/
public static GitLabApi oauth2Login(String url, String username, String password,
String secretToken, Map<String, Object> clientConfigProperties, boolean ignoreCertificateErrors)
throws GitLabApiException {
@Deprecated
public static GitLabApi oauth2Login(String url, String username, String password, String secretToken,
Map<String, Object> clientConfigProperties, boolean ignoreCertificateErrors) throws GitLabApiException {
return (GitLabApi.oauth2Login(ApiVersion.V4, url, username, password, secretToken, clientConfigProperties, ignoreCertificateErrors));
}
/**
* <p>Logs into GitLab using OAuth2 with the provided {@code username} and {@code password},
* and creates a new {@code GitLabApi} instance using returned access token.</p>
*
* @param url GitLab URL
* @param username user name for which private token should be obtained
* @param password a CharSequence containing the password for a given {@code username}
* @param secretToken use this token to validate received payloads
* @param clientConfigProperties Map instance with additional properties for the Jersey client connection
* @param ignoreCertificateErrors if true will set up the Jersey system ignore SSL certificate errors
* @return new {@code GitLabApi} instance configured for a user-specific token
* @throws GitLabApiException GitLabApiException if any exception occurs during execution
*/
public static GitLabApi oauth2Login(String url, String username, CharSequence password, String secretToken,
Map<String, Object> clientConfigProperties, boolean ignoreCertificateErrors) throws GitLabApiException {
return (GitLabApi.oauth2Login(ApiVersion.V4, url, username, password, secretToken, clientConfigProperties, ignoreCertificateErrors));
}
/**
* <p>Logs into GitLab using OAuth2 with the provided {@code username} and {@code password},
* and creates a new {@code GitLabApi} instance using returned access token.</p>
*
* @param url GitLab URL
* @param username user name for which private token should be obtained
* @param password a char array holding the password for a given {@code username}
* @param secretToken use this token to validate received payloads
* @param clientConfigProperties Map instance with additional properties for the Jersey client connection
* @param ignoreCertificateErrors if true will set up the Jersey system ignore SSL certificate errors
* @return new {@code GitLabApi} instance configured for a user-specific token
* @throws GitLabApiException GitLabApiException if any exception occurs during execution
*/
public static GitLabApi oauth2Login(String url, String username, char[] password, String secretToken,
Map<String, Object> clientConfigProperties, boolean ignoreCertificateErrors) throws GitLabApiException {
try (SecretString secretPassword = new SecretString(password)) {
return (GitLabApi.oauth2Login(ApiVersion.V4, url, username, secretPassword,
secretToken, clientConfigProperties, ignoreCertificateErrors));
}
}
/**
* <p>Logs into GitLab using OAuth2 with the provided {@code username} and {@code password},
* and creates a new {@code GitLabApi} instance using returned access token.</p>
*
* @param url GitLab URL
* @param apiVersion the ApiVersion specifying which version of the API to use
* @param username user name for which private token should be obtained
* @param password a char array holding the password for a given {@code username}
* @param secretToken use this token to validate received payloads
* @param clientConfigProperties Map instance with additional properties for the Jersey client connection
* @param ignoreCertificateErrors if true will set up the Jersey system ignore SSL certificate errors
* @return new {@code GitLabApi} instance configured for a user-specific token
* @throws GitLabApiException GitLabApiException if any exception occurs during execution
*/
public static GitLabApi oauth2Login(ApiVersion apiVersion, String url, String username, char[] password, String secretToken,
Map<String, Object> clientConfigProperties, boolean ignoreCertificateErrors) throws GitLabApiException {
try (SecretString secretPassword = new SecretString(password)) {
return (GitLabApi.oauth2Login(apiVersion, url, username, secretPassword,
secretToken, clientConfigProperties, ignoreCertificateErrors));
}
}
/**
* <p>Logs into GitLab using OAuth2 with the provided {@code username} and {@code password},
* and creates a new {@code GitLabApi} instance using returned access token.</p>
......@@ -148,7 +283,7 @@ public class GitLabApi {
* @return new {@code GitLabApi} instance configured for a user-specific token
* @throws GitLabApiException GitLabApiException if any exception occurs during execution
*/
public static GitLabApi oauth2Login(ApiVersion apiVersion, String url, String username, String password,
public static GitLabApi oauth2Login(ApiVersion apiVersion, String url, String username, CharSequence password,
String secretToken, Map<String, Object> clientConfigProperties, boolean ignoreCertificateErrors)
throws GitLabApiException {
......@@ -167,19 +302,17 @@ public class GitLabApi {
}
}
GitLabApiForm formData = new GitLabApiForm()
.withParam("grant_type", "password", true)
.withParam("username", username, true)
.withParam("password", password, true);
try (Oauth2LoginStreamingOutput stream = new Oauth2LoginStreamingOutput(username, password)) {
Response response = new Oauth2Api(gitLabApi).post(Response.Status.OK, formData, "oauth", "token");
OauthTokenResponse oauthToken = response.readEntity(OauthTokenResponse.class);
gitLabApi = new GitLabApi(apiVersion, url, TokenType.ACCESS, oauthToken.getAccessToken(), secretToken, clientConfigProperties);
if (ignoreCertificateErrors) {
gitLabApi.setIgnoreCertificateErrors(true);
}
Response response = new Oauth2Api(gitLabApi).post(Response.Status.OK, stream, MediaType.APPLICATION_JSON, "oauth", "token");
OauthTokenResponse oauthToken = response.readEntity(OauthTokenResponse.class);
gitLabApi = new GitLabApi(apiVersion, url, TokenType.ACCESS, oauthToken.getAccessToken(), secretToken, clientConfigProperties);
if (ignoreCertificateErrors) {
gitLabApi.setIgnoreCertificateErrors(true);
}
return (gitLabApi);
return (gitLabApi);
}
}
/**
......@@ -195,7 +328,9 @@ public class GitLabApi {
* @param password password for a given {@code username}
* @return new {@code GitLabApi} instance configured for a user-specific token
* @throws GitLabApiException GitLabApiException if any exception occurs during execution
* @deprecated As of release 4.8.7, will be removed in 4.9.0
*/
@Deprecated
public static GitLabApi login(ApiVersion apiVersion, String url, String username, String password) throws GitLabApiException {
return (GitLabApi.login(apiVersion, url, username, password, false));
}
......@@ -212,7 +347,9 @@ public class GitLabApi {
* @param password password for a given {@code username}
* @return new {@code GitLabApi} instance configured for a user-specific token
* @throws GitLabApiException GitLabApiException if any exception occurs during execution
* @deprecated As of release 4.8.7, will be removed in 4.9.0
*/
@Deprecated
public static GitLabApi login(String url, String username, String password) throws GitLabApiException {
return (GitLabApi.login(ApiVersion.V4, url, username, password, false));
}
......@@ -231,7 +368,9 @@ public class GitLabApi {
* @param ignoreCertificateErrors if true will set up the Jersey system ignore SSL certificate errors
* @return new {@code GitLabApi} instance configured for a user-specific token
* @throws GitLabApiException GitLabApiException if any exception occurs during execution
* @deprecated As of release 4.8.7, will be removed in 4.9.0
*/
@Deprecated
public static GitLabApi login(ApiVersion apiVersion, String url, String username, String password, boolean ignoreCertificateErrors) throws GitLabApiException {
GitLabApi gitLabApi = new GitLabApi(apiVersion, url, (String)null);
......@@ -273,7 +412,9 @@ public class GitLabApi {
* @param ignoreCertificateErrors if true will set up the Jersey system ignore SSL certificate errors
* @return new {@code GitLabApi} instance configured for a user-specific token
* @throws GitLabApiException GitLabApiException if any exception occurs during execution
* @deprecated As of release 4.8.7, will be removed in 4.9.0
*/
@Deprecated
public static GitLabApi login(String url, String username, String password, boolean ignoreCertificateErrors) throws GitLabApiException {
return (GitLabApi.login(ApiVersion.V4, url, username, password, ignoreCertificateErrors));
}
......@@ -287,7 +428,7 @@ public class GitLabApi {
* @param password password for a given {@code username}
* @return new {@code GitLabApi} instance configured for a user-specific token
* @throws GitLabApiException GitLabApiException if any exception occurs during execution
* @deprecated As of release 4.2.0, replaced by {@link #login(String, String, String)}, will be removed in 5.0.0
* @deprecated As of release 4.2.0, replaced by {@link #login(String, String, String)}, will be removed in 4.9.0
*/
@Deprecated
public static GitLabApi create(String url, String username, String password) throws GitLabApiException {
......@@ -301,8 +442,9 @@ public class GitLabApi {
* <strong>NOTE</strong>: For GitLab servers 10.2 and above this method will always return null.
*
* @return the Session instance
* @deprecated This method will be removed in Release 5.0.0
* @deprecated This method will be removed in Release 4.9.0
*/
@Deprecated
public Session getSession() {
return session;
}
......
......@@ -25,6 +25,7 @@ import javax.ws.rs.core.Form;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.StreamingOutput;
import org.gitlab4j.api.Constants.TokenType;
import org.gitlab4j.api.GitLabApi.ApiVersion;
......@@ -434,6 +435,21 @@ public class GitLabApiClient {
return (invocation(url, null).post(entity));
}
/**
* Perform an HTTP POST call with the specified StreamingOutput, MediaType, and path objects, returning
* a ClientResponse instance with the data returned from the endpoint.
*
* @param stream the StreamingOutput instance that contains the POST data
* @param mediaType the content-type of the POST data
* @param pathArgs variable list of arguments used to build the URI
* @return a ClientResponse instance with the data returned from the endpoint
* @throws IOException if an error occurs while constructing the URL
*/
protected Response post(StreamingOutput stream, String mediaType, Object... pathArgs) throws IOException {
URL url = getApiUrl(pathArgs);
return (invocation(url, null).post(Entity.entity(stream, mediaType)));
}
/**
* Perform an HTTP PUT call with the specified form data and path objects, returning
* a ClientResponse instance with the data returned from the endpoint.
......
package org.gitlab4j.api.utils;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintWriter;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.StreamingOutput;
/**
* This StreamingOutput implementation is utilized to send a OAuth2 token request
* in a secure manner. The password is never copied to a String, instead it is
* contained in a SecretString that is cleared when an instance of this class is finalized.
*/
public class Oauth2LoginStreamingOutput implements StreamingOutput, AutoCloseable {
private final String username;
private final SecretString password;
public Oauth2LoginStreamingOutput(String username, CharSequence password) {
this.username = username;
this.password = new SecretString(password);
}
public Oauth2LoginStreamingOutput(String username, char[] password) {
this.username = username;
this.password = new SecretString(password);
}
@Override
public void write(OutputStream output) throws IOException, WebApplicationException {
PrintWriter writer = new PrintWriter(output);
writer.write("{ ");
writer.write("\"grant_type\": \"password\", ");
writer.write("\"username\": \"" + username + "\", ");
writer.write("\"password\": ");
// Output the quoted password
writer.write('"');
for (int i = 0, length = password.length(); i < length; i++) {
writer.write(password.charAt(i));
}
writer.write('"');
writer.write(" }");
writer.flush();
writer.close();
}
/**
* Clears the contained password's data.
*/
public void clearPassword() {
password.clear();
}
@Override
public void close() {
clearPassword();
}
@Override
public void finalize() throws Throwable {
clearPassword();
super.finalize();
}
}
\ No newline at end of file
package org.gitlab4j.api.utils;
import java.util.Arrays;
/**
* This class implements a CharSequence that can be cleared of it's contained characters.
* This class is utilized to pass around secrets (passwords) instead of a String instance.
*/
public class SecretString implements CharSequence, AutoCloseable {
private final char[] chars;
public SecretString(CharSequence charSequence) {
int length = charSequence.length();
chars = new char[length];
for (int i = 0; i < length; i++) {
chars[i] = charSequence.charAt(i);
}
}
public SecretString(char[] chars) {
this(chars, 0, chars.length);
}
public SecretString(char[] chars, int start, int end) {
this.chars = new char[end - start];
System.arraycopy(chars, start, this.chars, 0, this.chars.length);
}
@Override
public char charAt(int index) {
return chars[index];
}
@Override
public void close() {
clear();
}
@Override
public int length() {
return chars.length;
}
@Override
public CharSequence subSequence(int start, int end) {
return new SecretString(this.chars, start, end);
}
/**
* Clear the contents of this SecretString instance by setting each character to 0.
* This is automatically done in the finalize() method.
*/
public void clear() {
Arrays.fill(chars, '\0');
}
@Override
public void finalize() throws Throwable {
clear();
super.finalize();
}
}
\ No newline at end of file
......@@ -7,6 +7,7 @@ import static org.junit.Assume.assumeTrue;
import org.gitlab4j.api.GitLabApi.ApiVersion;
import org.gitlab4j.api.models.Version;
import org.gitlab4j.api.utils.SecretString;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
......@@ -47,19 +48,19 @@ public class TestGitLabLogin {
problems = "";
if (TEST_LOGIN_USERNAME == null || TEST_LOGIN_USERNAME.trim().length() == 0) {
if (TEST_LOGIN_USERNAME == null || TEST_LOGIN_USERNAME.trim().isEmpty()) {
problems += "TEST_LOGIN_USERNAME cannot be empty\n";
}
if (TEST_LOGIN_PASSWORD == null || TEST_LOGIN_PASSWORD.trim().length() == 0) {
if (TEST_LOGIN_PASSWORD == null || TEST_LOGIN_PASSWORD.trim().isEmpty()) {
problems += "TEST_LOGIN_PASSWORD cannot be empty\n";
}
if (TEST_HOST_URL == null || TEST_HOST_URL.trim().length() == 0) {
if (TEST_HOST_URL == null || TEST_HOST_URL.trim().isEmpty()) {
problems += "TEST_HOST_URL cannot be empty\n";
}
if (TEST_PRIVATE_TOKEN == null || TEST_PRIVATE_TOKEN.trim().length() == 0) {
if (TEST_PRIVATE_TOKEN == null || TEST_PRIVATE_TOKEN.trim().isEmpty()) {
problems += "TEST_PRIVATE_TOKEN cannot be empty\n";
}
......@@ -112,8 +113,8 @@ public class TestGitLabLogin {
@Test
public void testSessionFallover() throws GitLabApiException {
assumeFalse(hasSession);
@SuppressWarnings("deprecation")
GitLabApi gitLabApi = GitLabApi.login(ApiVersion.V4, TEST_HOST_URL, TEST_LOGIN_USERNAME, TEST_LOGIN_PASSWORD);
assertNotNull(gitLabApi);
Version version = gitLabApi.getVersion();
......@@ -121,11 +122,29 @@ public class TestGitLabLogin {
}
@Test
public void testOauth2Login() throws GitLabApiException {
public void testOauth2LoginWithStringPassword() throws GitLabApiException {
@SuppressWarnings("deprecation")
GitLabApi gitLabApi = GitLabApi.oauth2Login(TEST_HOST_URL, TEST_LOGIN_USERNAME, TEST_LOGIN_PASSWORD, null, null, true);
assertNotNull(gitLabApi);
Version version = gitLabApi.getVersion();
assertNotNull(version);
}
@Test
public void testOauth2LoginWithCharSequencePassword() throws GitLabApiException {
SecretString password = new SecretString(TEST_LOGIN_PASSWORD);
GitLabApi gitLabApi = GitLabApi.oauth2Login(TEST_HOST_URL, TEST_LOGIN_USERNAME, password, null, null, true);
assertNotNull(gitLabApi);
Version version = gitLabApi.getVersion();
assertNotNull(version);
}
@Test
public void testOauth2LoginWithCharArrayPassword() throws GitLabApiException {
char[] password = TEST_LOGIN_PASSWORD.toCharArray();
GitLabApi gitLabApi = GitLabApi.oauth2Login(TEST_HOST_URL, TEST_LOGIN_USERNAME, password, null, null, true);
assertNotNull(gitLabApi);
Version version = gitLabApi.getVersion();
assertNotNull(version);
}
}
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment