Skip to content

Commit 5701c12

Browse files
committed
Fix OAuth2 Autoritation Code with PKCE
1 parent ff11f1e commit 5701c12

5 files changed

Lines changed: 141 additions & 144 deletions

File tree

Lines changed: 15 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,31 @@
11
package it.eng.spagobi.rest.oauth2;
22

3-
import javax.servlet.http.HttpServletRequest;
4-
import javax.servlet.http.HttpServletResponse;
53
import javax.ws.rs.DefaultValue;
64
import javax.ws.rs.GET;
75
import javax.ws.rs.Path;
6+
import javax.ws.rs.Produces;
87
import javax.ws.rs.QueryParam;
9-
import javax.ws.rs.core.Context;
8+
import javax.ws.rs.core.MediaType;
9+
import javax.ws.rs.core.Response;
1010

1111
import org.apache.log4j.Logger;
12+
import org.json.JSONException;
13+
import org.json.JSONObject;
1214

15+
import it.eng.spagobi.api.AbstractSpagoBIResource;
1316
import it.eng.spagobi.rest.oauth2.dto.OAuth2TokenDTO;
1417
import it.eng.spagobi.security.OAuth2.OAuth2Client;
18+
import it.eng.spagobi.services.rest.annotations.PublicService;
1519

16-
import org.json.JSONException;
17-
import org.json.JSONObject;
18-
19-
//@PublicService
2020
@Path("/oauth2clientservice")
21-
public class OAuth2ClientService {
21+
public class OAuth2ClientService extends AbstractSpagoBIResource {
2222

23-
@Context
24-
private HttpServletRequest request;
25-
26-
@Context
27-
private HttpServletResponse response;
28-
2923
private static Logger logger = Logger.getLogger(OAuth2ClientService.class);
30-
24+
3125
@GET
32-
public OAuth2TokenDTO getOAuth2Client(@QueryParam("code") @DefaultValue("") String code) {
26+
@Produces(MediaType.APPLICATION_JSON)
27+
@PublicService
28+
public Response getOAuth2Client(@QueryParam("code") @DefaultValue("") String code) {
3329
OAuth2Client client = new OAuth2Client();
3430
String genToken = client.getAccessToken(code);
3531
OAuth2TokenDTO dto = new OAuth2TokenDTO();
@@ -43,8 +39,8 @@ public OAuth2TokenDTO getOAuth2Client(@QueryParam("code") @DefaultValue("") Stri
4339
logger.error("Cannot parse access token response", e);
4440
}
4541
}
46-
return dto;
42+
return Response.ok(dto).build();
4743
}
48-
49-
44+
45+
5046
}
Lines changed: 14 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,40 @@
11
package it.eng.spagobi.rest.oauth2;
22

3-
import javax.servlet.http.HttpServletRequest;
4-
import javax.servlet.http.HttpServletResponse;
53
import javax.ws.rs.GET;
64
import javax.ws.rs.Path;
7-
import javax.ws.rs.core.Context;
8-
9-
import org.apache.log4j.Logger;
5+
import javax.ws.rs.Produces;
6+
import javax.ws.rs.core.MediaType;
7+
import javax.ws.rs.core.Response;
108

9+
import it.eng.spagobi.api.AbstractSpagoBIResource;
1110
import it.eng.spagobi.rest.oauth2.dto.OAuth2ConfigDTO;
1211
import it.eng.spagobi.security.OAuth2.OAuth2Config;
12+
import it.eng.spagobi.services.rest.annotations.PublicService;
1313

1414

15-
//@PublicService
1615
@Path("/oauth2configservice")
17-
public class OAuth2ConfigService {
18-
@Context
19-
private HttpServletRequest request;
20-
21-
@Context
22-
private HttpServletResponse response;
23-
24-
private static Logger logger = Logger.getLogger(OAuth2ConfigService.class);
25-
16+
public class OAuth2ConfigService extends AbstractSpagoBIResource {
17+
2618
@GET
27-
public OAuth2ConfigDTO getOAuth2Config() {
19+
@Produces(MediaType.APPLICATION_JSON)
20+
@PublicService
21+
public Response getOAuth2Config() {
2822
OAuth2Config oauth2Config = OAuth2Config.getInstance();
2923
String authorizeUrl = oauth2Config.getAuthorizeUrl();
3024
String accessTokenUrl = oauth2Config.getAccessTokenUrl();
3125
String clientId = oauth2Config.getClientId();
3226
String redirectUrl = oauth2Config.getRedirectUrl();
3327
String scopes = oauth2Config.getScopes();
34-
28+
3529
OAuth2ConfigDTO dto = new OAuth2ConfigDTO();
3630
dto.setAccessTokenUrl(accessTokenUrl);
3731
dto.setAuthorizeUrl(authorizeUrl);
3832
dto.setClientId(clientId);
3933
dto.setRedirectUrl(redirectUrl);
4034
dto.setScopes(scopes);
41-
return dto;
35+
return Response.ok(dto).build();
4236
}
43-
37+
38+
4439

4540
}

knowage/src/main/webapp/oauth2/pkce/flow.jsp

Lines changed: 108 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -27,118 +27,120 @@ From https://github.com/curityio/pkce-javascript-example
2727
<title>OAuth2 PKCE flow</title>
2828
</head>
2929
<body>
30-
<script>
31-
32-
var oauth2Config = null;
33-
34-
var xhrOAuth2C = new XMLHttpRequest();
35-
36-
xhrOAuth2C.onload = function() {
37-
var response = xhrOAuth2C.response;
38-
39-
if (xhrOAuth2C.status == 200) {
40-
oauthConfig = response;
41-
} else {
42-
alert("Error: " + response.error_description + " (" + response.error + ")");
43-
}
44-
};
45-
xhrOAuth2C.responseType = 'json';
46-
xhrOAuth2C.open("GET", '/oauth2configservice', true);
47-
//xhrOAuth2C.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
48-
xhrOAuth2C.send();
49-
50-
const authorizeEndpoint = oauth2Config.authorizeUrl;
51-
const tokenEndpoint = oauth2Config.accessTokenUrl;
52-
const clientId = oauth2Config.clientId;
53-
const redirectUri = oauth2Config.redirectUrl;
54-
55-
if (window.location.search) {
56-
var args = new URLSearchParams(window.location.search);
57-
var code = args.get("code");
58-
var state = args.get("state");
59-
60-
if (code) {
61-
if (window.sessionStorage.getItem("state") !== state){
62-
throw Error("Probable session hijacking attack!");
63-
}
64-
65-
var xhr = new XMLHttpRequest();
66-
67-
xhr.onload = function() {
68-
var response = xhr.response;
69-
70-
if (xhr.status == 200) {
71-
// storing id_token for later usage (on logout)
72-
window.sessionStorage.setItem("id_token", response.id_token);
73-
74-
var lastRedirectUri = window.location.href.split('?')[0];
75-
var args = new URLSearchParams({
76-
PAGE : "LoginPage",
77-
NEW_SESSION : "TRUE",
78-
access_token: response.access_token
79-
});
80-
window.location = lastRedirectUri + "?" + args;
81-
} else {
82-
alert("Error: " + response.error_description + " (" + response.error + ")");
83-
}
84-
85-
document.getElementById("result").innerHTML = message;
86-
};
87-
xhr.responseType = 'json';
88-
xhr.open("POST", tokenEndpoint, true);
89-
xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
90-
xhr.send(new URLSearchParams({
91-
client_id: clientId,
92-
code_verifier: window.sessionStorage.getItem("code_verifier"),
93-
grant_type: "authorization_code",
94-
redirect_uri: redirectUri,
95-
code: code,
96-
state: state
97-
}));
98-
} else {
99-
startOauth2Flow();
30+
<script>
31+
start();
32+
33+
async function start() {
34+
if (window.location.search) {
35+
var args = new URLSearchParams(window.location.search);
36+
var code = args.get("code");
37+
var state = args.get("state");
38+
if (code) {
39+
const oauth2Config = await fetchConfig();
40+
if (window.sessionStorage.getItem("state") !== state){
41+
throw Error("Probable session hijacking attack!");
42+
}
43+
fetch(oauth2Config.accessTokenUrl, {
44+
method: 'POST',
45+
headers: {
46+
'Content-Type': 'application/x-www-form-urlencoded'
47+
},
48+
body: new URLSearchParams({
49+
client_id: oauth2Config.clientId,
50+
code_verifier: window.sessionStorage.getItem("code_verifier"),
51+
grant_type: "authorization_code",
52+
redirect_uri: oauth2Config.redirectUrl,
53+
code: code,
54+
state: state
55+
})
56+
})
57+
.then(response => response.json().then(data => ({ status: response.status, body: data })))
58+
.then(({ status, body }) => {
59+
if (status === 200) {
60+
// storing id_token for later usage (on logout)
61+
window.sessionStorage.setItem("id_token", body.id_token);
62+
63+
const lastRedirectUri = window.location.href.split('?')[0];
64+
const args = new URLSearchParams({
65+
PAGE: "LoginPage",
66+
NEW_SESSION: "TRUE",
67+
access_token: body.access_token
68+
});
69+
70+
window.location = lastRedirectUri + "?" + args;
71+
} else {
72+
alert(`Error: ${body.error_description} (${body.error})`);
73+
}
74+
75+
})
76+
.catch(error => {
77+
console.error("Errore nella richiesta fetch:", error);
78+
});
79+
80+
} else {
81+
startOauth2Flow();
82+
}
10083
}
10184
}
102-
103-
function startOauth2Flow() {
104-
var state = generateRandomString(64);
105-
var codeVerifier = generateRandomString(128);
106-
107-
generateCodeChallenge(codeVerifier).then(function(codeChallenge) {
108-
window.sessionStorage.setItem("state", state);
109-
window.sessionStorage.setItem("code_verifier", codeVerifier);
110-
111-
var args = new URLSearchParams({
112-
response_type: "code",
113-
client_id: clientId,
114-
code_challenge_method: "S256",
115-
code_challenge: codeChallenge,
116-
state: state,
117-
redirect_uri: redirectUri,
118-
scope: "openid profile"
119-
});
120-
window.location = authorizeEndpoint + "?" + args;
121-
});
85+
86+
async function fetchConfig() {
87+
const response = await fetch('/knowage/restful-services/oauth2configservice', {
88+
method: 'GET'
89+
})
90+
91+
if (!response.ok) {
92+
throw new Error("Errore nella chiamata oauth2configservice");
93+
}
94+
const config = await response.json();
95+
return config;
12296
}
123-
124-
async function generateCodeChallenge(codeVerifier) {
125-
var digest = await crypto.subtle.digest("SHA-256",
126-
new TextEncoder().encode(codeVerifier));
127-
128-
return btoa(String.fromCharCode(...new Uint8Array(digest)))
129-
.replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_')
97+
98+
async function startOauth2Flow() {
99+
const oauth2Config = await fetchConfig();
100+
const state = generateRandomString(64);
101+
const verifier = generateRandomString(128);
102+
const challenge = await generateCodeChallenge(verifier);
103+
sessionStorage.setItem("state", state);
104+
sessionStorage.setItem("code_verifier", verifier);
105+
var args = new URLSearchParams({
106+
response_type: "code",
107+
client_id: oauth2Config.clientId,
108+
code_challenge_method: "S256",
109+
code_challenge: challenge,
110+
state: state,
111+
redirect_uri: oauth2Config.redirectUrl,
112+
scope: "openid profile"
113+
});
114+
window.location = oauth2Config.authorizeUrl + "?" + args;
115+
130116
}
117+
131118
132-
function generateRandomString(length) {
133-
var text = "";
134-
var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
135-
136-
for (var i = 0; i < length; i++) {
137-
text += possible.charAt(Math.floor(Math.random() * possible.length));
138-
}
119+
// Funzione per generare una stringa casuale (code_verifier)
120+
function generateRandomString(length) {
121+
  const charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~';
122+
  let result = '';
123+
  const array = new Uint8Array(length);
124+
  window.crypto.getRandomValues(array);
125+
  for (let i = 0; i < array.length; i++) {
126+
    result += charset[array[i] % charset.length];
127+
  }
128+
  return result;
129+
}
130+
131+
132+
// Funzione per generare il code_challenge (SHA256 + base64url)
133+
async function generateCodeChallenge(codeVerifier) {
134+
  const encoder = new TextEncoder();
135+
  const data = encoder.encode(codeVerifier);
136+
  const digest = await window.crypto.subtle.digest('SHA-256', data);
137+
  const base64url = btoa(String.fromCharCode(...new Uint8Array(digest)))
138+
    .replace(/\+/g, '-')
139+
    .replace(/\//g, '_')
140+
    .replace(/=+$/, '');
141+
  return base64url;
142+
}
139143
140-
return text;
141-
}
142144
143145
</script>
144146
</body>

knowageutils/src/main/java/it/eng/spagobi/security/OAuth2/OAuth2Filter.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,8 @@ public void init(FilterConfig fConfig) throws ServletException {
201201
flowManager = new ImplicitFlowManager();
202202
break;
203203
case PKCE:
204+
flowManager = new ClassicFlowManager();
205+
break;
204206
case AUTHORIZATION_CODE:
205207
flowManager = new ClassicFlowManager();
206208
break;

knowageutils/src/main/java/it/eng/spagobi/security/utils/AntiCsrfFilter.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,8 @@ private boolean excludeCheck(String path) {
8282
urlToExclude.add("/start/generatePPTV2");
8383
urlToExclude.add("/start/generatePPT");
8484
urlToExclude.add("/start/generateDOC");
85+
urlToExclude.add("/oauth2configservice");
86+
urlToExclude.add("/oauth2clientservice");
8587

8688
return urlToExclude.contains(path) || path.startsWith("/1.0/ai/") || path.startsWith("/dossier/activity/");
8789
}

0 commit comments

Comments
 (0)