Skip to content

Commit c5b5299

Browse files
committed
WIP: support for locally refreshing access tokens
i.e. without Jira knowing (since it cannot deal with that in version 7.x) This has become necessary since GitLab 15 enforces access token expiry after 2h, which need to be refreshed, then.
1 parent 7bed9b0 commit c5b5299

File tree

8 files changed

+361
-14
lines changed

8 files changed

+361
-14
lines changed

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
<groupId>com.dkaedv</groupId>
66
<artifactId>glghproxy</artifactId>
7-
<version>0.5.3-SNAPSHOT</version>
7+
<version>0.6.0-SNAPSHOT</version>
88
<packaging>war</packaging>
99

1010
<name>glghproxy</name>

src/main/java/com/dkaedv/glghproxy/Utils.java

Lines changed: 87 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,21 +8,39 @@
88
//
99
package com.dkaedv.glghproxy;
1010

11+
import java.io.BufferedInputStream;
12+
import java.io.BufferedOutputStream;
13+
import java.io.File;
14+
import java.io.FileInputStream;
15+
import java.io.FileNotFoundException;
16+
import java.io.FileOutputStream;
17+
import java.io.IOException;
18+
import java.nio.file.Files;
19+
import java.nio.file.attribute.PosixFilePermissions;
1120
import java.util.List;
1221
import java.util.Optional;
22+
import java.util.Properties;
23+
24+
import javax.validation.constraints.NotNull;
1325

1426
import org.apache.commons.logging.Log;
1527
import org.apache.commons.logging.LogFactory;
1628
import org.gitlab.api.models.GitlabUser;
29+
import org.springframework.beans.factory.annotation.Value;
30+
import org.springframework.stereotype.Component;
31+
32+
import com.dkaedv.glghproxy.githubentity.AccessToken;
1733

1834
/**
1935
* A place for some helper methods.
2036
*/
37+
@Component
2138
public class Utils {
2239

2340
private final static Log LOG = LogFactory.getLog(Utils.class);
2441

25-
private Utils() {}
42+
@Value("${stateFile}")
43+
private String stateFilePath;
2644

2745
/**
2846
* Returns the first user of the given list matching the givern email or username.
@@ -57,4 +75,72 @@ public static GitlabUser findSingleUser(List<GitlabUser> users, String emailOrUs
5775
// LOG.warn("input user: " + users.get(0).getUsername() + " : " + users.get(0).getEmail());
5876
return null;
5977
}
78+
79+
public synchronized void storeTokenToProperties(@NotNull AccessToken token) throws IllegalStateException {
80+
Properties properties = new Properties();
81+
properties.setProperty("accessToken", token.getAccess_token());
82+
properties.setProperty("refreshToken", token.getRefresh_token());
83+
properties.setProperty("expiresIn", String.valueOf(token.getExpires_in()));
84+
properties.setProperty("createdAt", String.valueOf(token.getCreated_at()));
85+
properties.setProperty("clientId", token.getClientId());
86+
properties.setProperty("clientSecret", token.getClientSecret());
87+
properties.setProperty("originalToken", token.getOriginalToken());
88+
properties.setProperty("redirectUri", token.getRedirectUri());
89+
90+
try {
91+
File stateFile = new File(stateFilePath);
92+
stateFile.getCanonicalFile().getParentFile().mkdirs();
93+
// ensure the file is created, so that we can set the permissions prior to writing it
94+
stateFile.createNewFile();
95+
Files.setPosixFilePermissions(stateFile.toPath(), PosixFilePermissions.fromString("rw-------"));
96+
properties.store(new BufferedOutputStream(new FileOutputStream(stateFile)), "glghproxy state");
97+
} catch (IOException e) {
98+
throw new IllegalStateException("Unable to store state file", e);
99+
};
100+
}
101+
102+
/**
103+
* Returns the token loaded from state, or <code>null</code> if there is no state yet
104+
* (which means, Jira must perform the OAuth dance, first).
105+
* @return the token or <code>null</code>
106+
* @throws IllegalStateException
107+
*/
108+
public synchronized AccessToken loadTokenFromProperties() throws IllegalStateException {
109+
Properties properties = new Properties();
110+
File stateFile = new File(stateFilePath);
111+
try {
112+
properties.load(new BufferedInputStream(new FileInputStream(stateFile)));
113+
AccessToken token = new AccessToken(
114+
properties.getProperty("accessToken"),
115+
properties.getProperty("refreshToken"),
116+
Long.valueOf(properties.getProperty("expiresIn")),
117+
Long.valueOf(properties.getProperty("createdAt"))
118+
);
119+
token.setClientId(properties.getProperty("clientId"));
120+
token.setClientSecret(properties.getProperty("clientSecret"));
121+
token.setOriginalToken(properties.getProperty("originalToken"));
122+
token.setRedirectUri(properties.getProperty("redirectUri"));
123+
124+
return token;
125+
} catch (FileNotFoundException e) {
126+
// no state file present, must login first
127+
try {
128+
LOG.warn("No state file present at " + stateFile.getCanonicalPath() + ", Jira will have to perform OAuth login first");
129+
} catch (IOException e1) {
130+
LOG.warn("No state file present at " + stateFile.toString() + ", Jira will have to perform OAuth login first", e1);
131+
}
132+
return null;
133+
} catch (IOException e) {
134+
throw new IllegalStateException("Unable to load state file " + stateFile.toString(), e);
135+
}
136+
}
137+
138+
public void deleteStateFile() {
139+
File stateFile = new File(stateFilePath);
140+
if (stateFile.exists()) {
141+
if (!stateFile.delete()) {
142+
throw new IllegalStateException("unable to delete state file");
143+
}
144+
}
145+
}
60146
}

src/main/java/com/dkaedv/glghproxy/controller/LoginController.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
import org.springframework.web.bind.annotation.RequestParam;
1515
import org.springframework.web.bind.annotation.ResponseBody;
1616

17+
import com.dkaedv.glghproxy.githubentity.AccessToken;
18+
import com.dkaedv.glghproxy.gitlabclient.GitlabSessionProvider;
1719
import com.dkaedv.glghproxy.gitlabclient.OAuthClient;
1820

1921
@Controller
@@ -25,6 +27,9 @@ public class LoginController {
2527
@Autowired
2628
private OAuthClient oauthClient;
2729

30+
@Autowired
31+
private GitlabSessionProvider gitlab;
32+
2833
private String redirectUri;
2934

3035
/**
@@ -76,6 +81,8 @@ public String accessToken(
7681
HttpServletRequest request
7782
) throws MalformedURLException {
7883

79-
return oauthClient.requestAccessToken(client_id, client_secret, code, buildCallbackUrl(request)).toString();
84+
AccessToken token = oauthClient.requestAccessToken(client_id, client_secret, code, buildCallbackUrl(request));
85+
gitlab.setToken(token);
86+
return token.toString();
8087
}
8188
}
Lines changed: 83 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,104 @@
11
package com.dkaedv.glghproxy.githubentity;
22

3+
import javax.validation.constraints.NotNull;
4+
5+
import com.fasterxml.jackson.annotation.JsonCreator;
6+
import com.fasterxml.jackson.annotation.JsonProperty;
7+
38
public class AccessToken {
4-
private String access_token;
5-
private String scope = "repo";
6-
private String token_type = "bearer";
7-
8-
public AccessToken() {}
9+
@NotNull
10+
private final String access_token;
11+
@NotNull
12+
private final String refresh_token;
13+
@NotNull
14+
private final Long expires_in;
15+
@NotNull
16+
private final Long created_at;
17+
18+
private String originalToken;
19+
private String clientId;
20+
private String clientSecret;
21+
private String redirectUri;
22+
23+
// constants -- these are in GitHub API
24+
private final String scope = "repo";
25+
private final String token_type = "bearer";
26+
27+
// public AccessToken() {}
928

10-
public AccessToken(String access_token) {
29+
@JsonCreator(mode = JsonCreator.Mode.PROPERTIES)
30+
public AccessToken(
31+
@JsonProperty("access_token") String access_token,
32+
@JsonProperty("refresh_token") String refresh_token,
33+
@JsonProperty("expires_in") Long expires_in,
34+
@JsonProperty("created_at") Long created_at
35+
) {
1136
this.access_token = access_token;
37+
this.refresh_token = refresh_token;
38+
this.expires_in = expires_in;
39+
this.created_at = created_at;
1240
}
1341
public String getAccess_token() {
1442
return access_token;
1543
}
44+
public String getRefresh_token() {
45+
return refresh_token;
46+
}
47+
/**
48+
* Returns an offset to @{@link #getCreated_at()} in seconds.
49+
*/
50+
public Long getExpires_in() {
51+
return expires_in;
52+
}
53+
/**
54+
* Returns a Unix timestamp in seconds, of when this token was issued.
55+
*/
56+
public Long getCreated_at() {
57+
return created_at;
58+
}
1659
public String getScope() {
1760
return scope;
1861
}
1962
public String getToken_type() {
2063
return token_type;
2164
}
2265

66+
/**
67+
* Returns back the token for Jira, so in GitHub API style
68+
*/
2369
@Override
2470
public String toString() {
2571
return "access_token=" + access_token + "&scope=" + scope + "&token_type=" + token_type;
2672
}
73+
74+
public void setClientId(String client_id) {
75+
clientId = client_id;
76+
}
77+
public String getClientId() {
78+
return clientId;
79+
}
80+
public void setClientSecret(String client_secret) {
81+
clientSecret = client_secret;
82+
}
83+
public String getClientSecret() {
84+
return clientSecret;
85+
}
86+
public void setRedirectUri(String redirect_uri) {
87+
redirectUri = redirect_uri;
88+
}
89+
public String getRedirectUri() {
90+
return redirectUri;
91+
}
92+
public void setOriginalToken(String originalToken) {
93+
if (originalToken == null) {
94+
throw new IllegalArgumentException("originalToken must not be null");
95+
}
96+
if (this.originalToken != null) {
97+
throw new IllegalStateException("original token already set");
98+
}
99+
this.originalToken = originalToken;
100+
}
101+
public String getOriginalToken() {
102+
return originalToken;
103+
}
27104
}

src/main/java/com/dkaedv/glghproxy/gitlabclient/GitlabSessionProvider.java

Lines changed: 65 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,19 @@
11
package com.dkaedv.glghproxy.gitlabclient;
22

33
import javax.annotation.PostConstruct;
4+
import javax.validation.constraints.NotNull;
45

56
import org.apache.commons.logging.Log;
67
import org.apache.commons.logging.LogFactory;
78
import org.gitlab.api.GitlabAPI;
89
import org.gitlab.api.TokenType;
10+
import org.springframework.beans.factory.annotation.Autowired;
911
import org.springframework.beans.factory.annotation.Value;
1012
import org.springframework.stereotype.Component;
1113

1214
import com.dkaedv.glghproxy.Constants;
15+
import com.dkaedv.glghproxy.Utils;
16+
import com.dkaedv.glghproxy.githubentity.AccessToken;
1317

1418
@Component
1519
public class GitlabSessionProvider {
@@ -18,14 +22,71 @@ public class GitlabSessionProvider {
1822
@Value("${gitlabUrl}")
1923
private String gitlabUrl;
2024

21-
public GitlabAPI connect(String authorizationHeader) {
22-
String token = authorizationHeader.replaceAll("token ", "");
23-
24-
GitlabAPI api = GitlabAPI.connect(gitlabUrl, token, TokenType.ACCESS_TOKEN);
25+
private AccessToken token;
26+
27+
@Autowired
28+
private OAuthClient oauthClient;
29+
30+
@Autowired
31+
private Utils utils;
32+
33+
@PostConstruct
34+
private void init() {
35+
token = utils.loadTokenFromProperties();
36+
}
37+
38+
/**
39+
* Called only once during initial login
40+
* @param token
41+
*/
42+
public void setToken(@NotNull AccessToken token) {
43+
this.token = token;
44+
utils.storeTokenToProperties(token);
45+
}
46+
47+
public synchronized GitlabAPI connect(String authorizationHeader) {
48+
if (token == null) {
49+
throw new IllegalStateException("AccessToken token must not be null at this point");
50+
}
51+
52+
// Note: since GitLab 15 invalidates tokens every 2h, and Jira doesn't support this flow, we
53+
// - verify that the token provided by Jira "would be valid"
54+
// - use our own, refreshed token instead of the one provided by Jira
55+
String providedToken = authorizationHeader.replaceAll("token ", "");
56+
if (!isValidOriginalToken(providedToken)) {
57+
throw new IllegalArgumentException("the provided token does not match the original token from the oauth2 initialization");
58+
}
59+
60+
if (isExpired()) {
61+
LOG.info("Refreshing oauth2 token");
62+
token = refreshToken();
63+
utils.storeTokenToProperties(token);
64+
}
65+
66+
GitlabAPI api = GitlabAPI.connect(gitlabUrl, token.getAccess_token(), TokenType.ACCESS_TOKEN);
2567
api.ignoreCertificateErrors(Constants.IGNORE_SSL_ERRORS);
68+
2669
return api;
2770
}
2871

72+
private boolean isValidOriginalToken(String providedToken) {
73+
return token.getOriginalToken().equals(providedToken);
74+
}
75+
76+
private boolean isExpired() {
77+
long expiration = token.getCreated_at() + token.getExpires_in();
78+
long now = System.currentTimeMillis() / 1000;
79+
long extraBuffer = 10 * 60; // 10min
80+
if (now >= expiration - extraBuffer) {
81+
return true;
82+
}
83+
return false;
84+
}
85+
86+
private AccessToken refreshToken() {
87+
return oauthClient.refreshAccessToken(token);
88+
}
89+
2990
@PostConstruct
3091
public void logUrl() {
3192
LOG.info("Using Gitlab Base URL: " + gitlabUrl);

0 commit comments

Comments
 (0)