Skip to content
GitLab
Explore
Sign in
Primary navigation
Search or go to…
Project
S
Service Specification exporting
Manage
Activity
Members
Labels
Plan
Issues
Issue boards
Milestones
Iterations
Wiki
Requirements
Code
Merge requests
Repository
Branches
Commits
Tags
Repository graph
Compare revisions
Snippets
Locked files
Build
Pipelines
Jobs
Pipeline schedules
Test cases
Artifacts
Deploy
Releases
Package Registry
Container Registry
Model registry
Operate
Environments
Terraform modules
Monitor
Incidents
Service Desk
Analyze
Value stream analytics
Contributor analytics
CI/CD analytics
Repository analytics
Code review analytics
Issue analytics
Insights
Model experiments
Help
Help
Support
GitLab documentation
Compare GitLab plans
Community forum
Contribute to GitLab
Provide feedback
Keyboard shortcuts
?
Snippets
Groups
Projects
Show more breadcrumbs
OSL
utilities
Service Specification exporting
Merge requests
!3
Develop
Code
Review changes
Check out branch
Download
Patches
Plain diff
Merged
Develop
develop
into
main
Overview
0
Commits
3
Pipelines
0
Changes
7
Merged
trantzas
requested to merge
develop
into
main
1 month ago
Overview
0
Commits
3
Pipelines
0
Changes
7
Expand
0
0
Merge request reports
Compare
main
main (base)
and
latest version
latest version
773fe32e
3 commits,
1 month ago
7 files
+
811
−
62
Inline
Compare changes
Side-by-side
Inline
Show whitespace changes
Show one file at a time
Some changes are not shown
For a faster browsing experience, some files are collapsed by default.
Expand all files
Files
7
Search (e.g. *.vue) (Ctrl+P)
src/main/java/org/osl/etsi/util/KeycloakAuthenticator.java
0 → 100644
+
175
−
0
Options
package
org.osl.etsi.util
;
import
com.fasterxml.jackson.databind.JsonNode
;
import
com.fasterxml.jackson.databind.ObjectMapper
;
import
org.slf4j.Logger
;
import
org.slf4j.LoggerFactory
;
import
java.io.IOException
;
import
java.net.URI
;
import
java.net.URLEncoder
;
import
java.net.http.HttpClient
;
import
java.net.http.HttpRequest
;
import
java.net.http.HttpResponse
;
import
java.nio.charset.StandardCharsets
;
import
java.time.Instant
;
import
java.util.Properties
;
public
class
KeycloakAuthenticator
{
private
static
final
Logger
logger
=
LoggerFactory
.
getLogger
(
KeycloakAuthenticator
.
class
.
getName
());
private
final
Properties
config
;
private
final
HttpClient
client
;
private
final
ObjectMapper
objectMapper
;
private
String
currentToken
;
private
long
tokenExpiryTime
;
private
long
tokenRefreshBufferSeconds
;
/**
* Constructs a KeycloakAuthenticator with the specified configuration file path.
*
* @param configFilePath The file path to the configuration properties file.
* @throws IOException If there is an error loading the configuration file.
*/
public
KeycloakAuthenticator
(
Properties
config
)
throws
IOException
{
this
.
config
=
config
;
this
.
client
=
HttpClient
.
newHttpClient
();
this
.
objectMapper
=
new
ObjectMapper
();
setTokenRefreshBufferSeconds
();
}
/**
* Loads the configuration properties from the specified file path.
*
* @param configFilePath The file path to the configuration properties file.
* @return The token refresh buffer time in seconds.
* @throws IOException If the configuration file cannot be read.
*/
private
void
setTokenRefreshBufferSeconds
()
throws
IOException
{
// Load the token refresh buffer time, defaulting to 60 seconds if not specified
String
bufferSecondsStr
=
this
.
config
.
getProperty
(
"token.refresh.buffer.seconds"
,
"60"
);
long
bufferSeconds
;
try
{
bufferSeconds
=
Long
.
parseLong
(
bufferSecondsStr
);
if
(
bufferSeconds
<
0
)
{
throw
new
NumberFormatException
(
"Buffer seconds cannot be negative."
);
}
}
catch
(
NumberFormatException
ex
)
{
logger
.
warn
(
"Invalid token.refresh.buffer.seconds value: "
+
bufferSecondsStr
+
". Using default of 60 seconds."
);
bufferSeconds
=
60
;
}
logger
.
info
(
"Token refresh buffer set to "
+
bufferSeconds
+
" seconds."
);
this
.
tokenRefreshBufferSeconds
=
bufferSeconds
;
}
/**
* Retrieves a valid access token. If the current token is expired or not present,
* it authenticates with Keycloak to obtain a new one.
*
* @return A valid access token as a String.
* @throws IOException If an I/O error occurs during authentication.
* @throws InterruptedException If the HTTP request is interrupted.
*/
public
synchronized
String
getToken
()
throws
IOException
,
InterruptedException
{
long
currentEpochSeconds
=
Instant
.
now
().
getEpochSecond
();
if
(
currentToken
!=
null
&&
currentEpochSeconds
<
(
tokenExpiryTime
-
tokenRefreshBufferSeconds
))
{
logger
.
info
(
"Using cached token. Token expires at "
+
Instant
.
ofEpochSecond
(
tokenExpiryTime
));
return
currentToken
;
}
else
{
logger
.
info
(
"Cached token is missing or nearing expiration. Authenticating to obtain a new token."
);
return
authenticateAndGetToken
();
}
}
/**
* Authenticates with Keycloak and retrieves a new access token.
*
* @return The new access token as a String.
* @throws IOException If the authentication request fails.
* @throws InterruptedException If the HTTP request is interrupted.
*/
private
String
authenticateAndGetToken
()
throws
IOException
,
InterruptedException
{
String
keycloakUrl
=
config
.
getProperty
(
"keycloak.url"
);
String
clientId
=
config
.
getProperty
(
"client.id"
);
String
clientSecret
=
config
.
getProperty
(
"client.secret"
);
String
username
=
config
.
getProperty
(
"username"
);
String
password
=
config
.
getProperty
(
"password"
);
// Validate required properties
if
(
keycloakUrl
==
null
||
clientId
==
null
||
username
==
null
||
password
==
null
)
{
String
errorMsg
=
"Missing required configuration properties."
;
logger
.
error
(
errorMsg
);
throw
new
IOException
(
errorMsg
);
}
// Build the form data with URL encoding
String
form
=
buildFormData
(
clientId
,
clientSecret
,
username
,
password
);
HttpRequest
request
=
HttpRequest
.
newBuilder
()
.
uri
(
URI
.
create
(
keycloakUrl
))
.
header
(
"Content-Type"
,
"application/x-www-form-urlencoded"
)
.
POST
(
HttpRequest
.
BodyPublishers
.
ofString
(
form
))
.
build
();
logger
.
info
(
"Sending authentication request to Keycloak."
);
HttpResponse
<
String
>
response
=
client
.
send
(
request
,
HttpResponse
.
BodyHandlers
.
ofString
());
if
(
response
.
statusCode
()
==
200
)
{
JsonNode
responseJson
=
objectMapper
.
readTree
(
response
.
body
());
currentToken
=
responseJson
.
get
(
"access_token"
).
asText
();
long
expiresIn
=
responseJson
.
get
(
"expires_in"
).
asLong
();
tokenExpiryTime
=
Instant
.
now
().
getEpochSecond
()
+
expiresIn
;
logger
.
info
(
"Authentication successful. Token obtained. Token expires in "
+
expiresIn
+
" seconds."
);
return
currentToken
;
}
else
{
String
errorMsg
=
"Authentication failed: HTTP status "
+
response
.
statusCode
()
+
": "
+
response
.
body
();
logger
.
error
(
errorMsg
);
throw
new
IOException
(
errorMsg
);
}
}
/**
* Builds the URL-encoded form data for the authentication request.
*
* @param clientId The client ID.
* @param clientSecret The client secret (optional).
* @param username The username.
* @param password The password.
* @return The URL-encoded form data as a String.
* @throws IOException If URL encoding fails.
*/
private
String
buildFormData
(
String
clientId
,
String
clientSecret
,
String
username
,
String
password
)
throws
IOException
{
StringBuilder
form
=
new
StringBuilder
();
form
.
append
(
"client_id="
).
append
(
urlEncode
(
clientId
));
if
(
clientSecret
!=
null
&&
!
clientSecret
.
isEmpty
())
{
form
.
append
(
"&client_secret="
).
append
(
urlEncode
(
clientSecret
));
}
form
.
append
(
"&username="
).
append
(
urlEncode
(
username
));
form
.
append
(
"&password="
).
append
(
urlEncode
(
password
));
form
.
append
(
"&grant_type=password"
);
return
form
.
toString
();
}
/**
* URL-encodes a string using UTF-8 encoding.
*
* @param value The string to encode.
* @return The URL-encoded string.
* @throws IOException If UTF-8 encoding is not supported.
*/
private
String
urlEncode
(
String
value
)
throws
IOException
{
try
{
return
URLEncoder
.
encode
(
value
,
StandardCharsets
.
UTF_8
.
toString
());
}
catch
(
Exception
ex
)
{
logger
.
error
(
"URL encoding failed for value: "
+
value
,
ex
);
throw
new
IOException
(
"URL encoding failed."
,
ex
);
}
}
}
Loading