Skip to content
This repository has been archived by the owner on Nov 29, 2021. It is now read-only.

Commit

Permalink
TK-392 connect Swagger with Keycloak for OAuth2 (#183)
Browse files Browse the repository at this point in the history
* Temporary workaround to avoid Quarkus issue quarkusio/quarkus#4766

Fix for quarkusio/quarkus#4766

* Add keycloak OAuth2 to the Swagger API
  • Loading branch information
Nicolas Martignole authored Jun 19, 2020
1 parent 7a786d4 commit 647e83a
Show file tree
Hide file tree
Showing 4 changed files with 230 additions and 72 deletions.
130 changes: 60 additions & 70 deletions infrastructure/keycloak/realm-export-acceptance.json
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,7 @@
"attributes": {}
}
],
"timekeeper-swagger": [],
"react-timekeeper-client": [],
"security-admin-console": [],
"admin-cli": [],
Expand Down Expand Up @@ -963,6 +964,63 @@
"offline_access",
"microprofile-jwt"
]
},
{
"id": "9eb912f2-7853-44f7-92bc-4a9972a52ae2",
"clientId": "timekeeper-swagger",
"description": "The client required by swagger, used to get read and write access on the API",
"surrogateAuthRequired": false,
"enabled": true,
"alwaysDisplayInConsole": false,
"clientAuthenticatorType": "client-secret",
"secret": "xbz32-any-secret-we-do-not-use-it",
"redirectUris": [
"https://acceptance.timekeeper.lunatech.fr/swagger-ui/oauth2-redirect.html",
"https://acceptance.timekeeper.lunatech.fr/oauth2-redirect.html"
],
"webOrigins": [],
"notBefore": 0,
"bearerOnly": false,
"consentRequired": false,
"standardFlowEnabled": true,
"implicitFlowEnabled": true,
"directAccessGrantsEnabled": true,
"serviceAccountsEnabled": false,
"publicClient": true,
"frontchannelLogout": false,
"protocol": "openid-connect",
"attributes": {
"saml.assertion.signature": "false",
"saml.force.post.binding": "false",
"saml.multivalued.roles": "false",
"saml.encrypt": "false",
"login_theme": "timekeeper",
"saml.server.signature": "false",
"saml.server.signature.keyinfo.ext": "false",
"exclude.session.state.from.auth.response": "false",
"saml_force_name_id_format": "false",
"saml.client.signature": "false",
"tls.client.certificate.bound.access.tokens": "false",
"saml.authnstatement": "false",
"display.on.consent.screen": "false",
"saml.onetimeuse.condition": "false"
},
"authenticationFlowBindingOverrides": {},
"fullScopeAllowed": true,
"nodeReRegistrationTimeout": -1,
"defaultClientScopes": [
"web-origins",
"role_list",
"profile",
"roles",
"email"
],
"optionalClientScopes": [
"address",
"phone",
"offline_access",
"microprofile-jwt"
]
}
],
"clientScopes": [
Expand Down Expand Up @@ -1501,76 +1559,8 @@
"eventsListeners": [
"jboss-logging"
],
"enabledEventTypes": [
"SEND_RESET_PASSWORD",
"UPDATE_CONSENT_ERROR",
"GRANT_CONSENT",
"REMOVE_TOTP",
"REVOKE_GRANT",
"UPDATE_TOTP",
"LOGIN_ERROR",
"CLIENT_LOGIN",
"RESET_PASSWORD_ERROR",
"IMPERSONATE_ERROR",
"CODE_TO_TOKEN_ERROR",
"CUSTOM_REQUIRED_ACTION",
"RESTART_AUTHENTICATION",
"IMPERSONATE",
"UPDATE_PROFILE_ERROR",
"LOGIN",
"UPDATE_PASSWORD_ERROR",
"CLIENT_INITIATED_ACCOUNT_LINKING",
"TOKEN_EXCHANGE",
"LOGOUT",
"REGISTER",
"CLIENT_REGISTER",
"IDENTITY_PROVIDER_LINK_ACCOUNT",
"UPDATE_PASSWORD",
"CLIENT_DELETE",
"FEDERATED_IDENTITY_LINK_ERROR",
"IDENTITY_PROVIDER_FIRST_LOGIN",
"CLIENT_DELETE_ERROR",
"VERIFY_EMAIL",
"CLIENT_LOGIN_ERROR",
"RESTART_AUTHENTICATION_ERROR",
"EXECUTE_ACTIONS",
"REMOVE_FEDERATED_IDENTITY_ERROR",
"TOKEN_EXCHANGE_ERROR",
"PERMISSION_TOKEN",
"SEND_IDENTITY_PROVIDER_LINK_ERROR",
"EXECUTE_ACTION_TOKEN_ERROR",
"SEND_VERIFY_EMAIL",
"EXECUTE_ACTIONS_ERROR",
"REMOVE_FEDERATED_IDENTITY",
"IDENTITY_PROVIDER_POST_LOGIN",
"IDENTITY_PROVIDER_LINK_ACCOUNT_ERROR",
"UPDATE_EMAIL",
"REGISTER_ERROR",
"REVOKE_GRANT_ERROR",
"EXECUTE_ACTION_TOKEN",
"LOGOUT_ERROR",
"UPDATE_EMAIL_ERROR",
"CLIENT_UPDATE_ERROR",
"UPDATE_PROFILE",
"CLIENT_REGISTER_ERROR",
"FEDERATED_IDENTITY_LINK",
"SEND_IDENTITY_PROVIDER_LINK",
"SEND_VERIFY_EMAIL_ERROR",
"RESET_PASSWORD",
"CLIENT_INITIATED_ACCOUNT_LINKING_ERROR",
"UPDATE_CONSENT",
"REMOVE_TOTP_ERROR",
"VERIFY_EMAIL_ERROR",
"SEND_RESET_PASSWORD_ERROR",
"CLIENT_UPDATE",
"CUSTOM_REQUIRED_ACTION_ERROR",
"IDENTITY_PROVIDER_POST_LOGIN_ERROR",
"UPDATE_TOTP_ERROR",
"CODE_TO_TOKEN",
"GRANT_CONSENT_ERROR",
"IDENTITY_PROVIDER_FIRST_LOGIN_ERROR"
],
"adminEventsEnabled": true,
"enabledEventTypes": [],
"adminEventsEnabled": false,
"adminEventsDetailsEnabled": false,
"identityProviders": [
{
Expand Down
58 changes: 58 additions & 0 deletions infrastructure/keycloak/realm-export-dev.json
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,7 @@
"attributes": {}
}
],
"timekeeper-swagger": [],
"react-timekeeper-client": [],
"security-admin-console": [],
"admin-cli": [],
Expand Down Expand Up @@ -913,6 +914,63 @@
"offline_access",
"microprofile-jwt"
]
},
{
"id": "9eb912f2-7853-44f7-92bc-4a9972a52ae2",
"clientId": "timekeeper-swagger",
"description": "The client required by swagger, used to get read and write access on the API",
"surrogateAuthRequired": false,
"enabled": true,
"alwaysDisplayInConsole": false,
"clientAuthenticatorType": "client-secret",
"secret": "xbz32-any-secret-we-do-not-use-it",
"redirectUris": [
"http://localhost:8081/swagger-ui/oauth2-redirect.html",
"http://localhost:8081/oauth2-redirect.html"
],
"webOrigins": [],
"notBefore": 0,
"bearerOnly": false,
"consentRequired": false,
"standardFlowEnabled": true,
"implicitFlowEnabled": true,
"directAccessGrantsEnabled": true,
"serviceAccountsEnabled": false,
"publicClient": true,
"frontchannelLogout": false,
"protocol": "openid-connect",
"attributes": {
"saml.assertion.signature": "false",
"saml.force.post.binding": "false",
"saml.multivalued.roles": "false",
"saml.encrypt": "false",
"login_theme": "timekeeper",
"saml.server.signature": "false",
"saml.server.signature.keyinfo.ext": "false",
"exclude.session.state.from.auth.response": "false",
"saml_force_name_id_format": "false",
"saml.client.signature": "false",
"tls.client.certificate.bound.access.tokens": "false",
"saml.authnstatement": "false",
"display.on.consent.screen": "false",
"saml.onetimeuse.condition": "false"
},
"authenticationFlowBindingOverrides": {},
"fullScopeAllowed": true,
"nodeReRegistrationTimeout": -1,
"defaultClientScopes": [
"web-origins",
"role_list",
"profile",
"roles",
"email"
],
"optionalClientScopes": [
"address",
"phone",
"offline_access",
"microprofile-jwt"
]
}
],
"clientScopes": [
Expand Down
45 changes: 43 additions & 2 deletions src/main/java/fr/lunatech/timekeeper/TimeKeeperApplication.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,13 @@

import org.eclipse.microprofile.openapi.annotations.ExternalDocumentation;
import org.eclipse.microprofile.openapi.annotations.OpenAPIDefinition;
import org.eclipse.microprofile.openapi.annotations.enums.SecuritySchemeIn;
import org.eclipse.microprofile.openapi.annotations.enums.SecuritySchemeType;
import org.eclipse.microprofile.openapi.annotations.info.Contact;
import org.eclipse.microprofile.openapi.annotations.info.Info;
import org.eclipse.microprofile.openapi.annotations.info.License;
import org.eclipse.microprofile.openapi.annotations.security.*;
import org.eclipse.microprofile.openapi.annotations.servers.Server;

import javax.ws.rs.ApplicationPath;
import javax.ws.rs.core.Application;
Expand All @@ -15,9 +20,45 @@
info = @Info(title = "TimeKeeper API",
description = "This API allows CRUD operations and interaction with TimeKeeper",
version = "1.0",
contact = @Contact(name = "TimeKeeper GitHub", url = "https://github.com/lunatech-labs/lunatech-timekeeper")),
externalDocs = @ExternalDocumentation(url = "https://lunatech.atlassian.net/wiki/spaces/INTRANET/pages/1609695253/Timekeeper", description = "Lunatech doc about TimeKeeper on Confluence")
contact = @Contact(name = "TimeKeeper GitHub", url = "https://github.com/lunatech-labs/lunatech-timekeeper"),
license = @License(name = "Apache 2.0", url = "http://www.apache.org/licenses/LICENSE-2.0.html")
),
servers = {
@Server(
url = "http://localhost:8081",
description = "DEV Server"
),
@Server(
url = "https://acceptance.api.timekeeper.lunatech.fr",
description = "ACCEPTANCE Server"
)
}
, security = {
@SecurityRequirement( name = "dev_timekeeperOAuth2" ,scopes = { "profile"}),
@SecurityRequirement( name = "acceptance_timekeeperOAuth2" ,scopes = { "profile"})
}
, externalDocs = @ExternalDocumentation(url = "https://lunatech.atlassian.net/wiki/spaces/INTRANET/pages/1609695253/Timekeeper", description = "Lunatech doc about TimeKeeper on Confluence")

)
// Quarkus Issue pending with redirect and OAuth2 here https://github.com/quarkusio/quarkus/issues/4766
// To fix this issue I extracted the swagger oauth2-redirect.html file and saved it in the timekeeper folder
@SecurityScheme(
securitySchemeName = "dev_timekeeperOAuth2",
type = SecuritySchemeType.OAUTH2,
description = "authentication for OAuth2 access",
flows = @OAuthFlows(
implicit = @OAuthFlow(authorizationUrl = "http://localhost:8082/auth/realms/Timekeeper/protocol/openid-connect/auth")
)
)
@SecurityScheme(
securitySchemeName = "acceptance_timekeeperOAuth2",
type = SecuritySchemeType.OAUTH2,
description = "authentication for OAuth2 access",
flows = @OAuthFlows(
implicit = @OAuthFlow(authorizationUrl = "https://acceptance.api.timekeeper.lunatech.fr/auth/realms/Timekeeper/protocol/openid-connect/auth")
)
)

public class TimeKeeperApplication extends Application {

}
69 changes: 69 additions & 0 deletions src/main/resources/META-INF/resources/oauth2-redirect.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<!doctype html>
<html lang="en-US">
<title>Timekeeper - Swagger UI: OAuth2 Redirect</title>
<body onload="run()">
<pre>Please wait...</pre>
</body>
</html>
<script>
'use strict';
function run () {
var oauth2 = window.opener.swaggerUIRedirectOauth2;
var sentState = oauth2.state;
var redirectUrl = oauth2.redirectUrl;
var isValid, qp, arr;

if (/code|token|error/.test(window.location.hash)) {
qp = window.location.hash.substring(1);
} else {
qp = location.search.substring(1);
}

arr = qp.split("&")
arr.forEach(function (v,i,_arr) { _arr[i] = '"' + v.replace('=', '":"') + '"';})
qp = qp ? JSON.parse('{' + arr.join() + '}',
function (key, value) {
return key === "" ? value : decodeURIComponent(value)
}
) : {}

isValid = qp.state === sentState

if ((
oauth2.auth.schema.get("flow") === "accessCode"||
oauth2.auth.schema.get("flow") === "authorizationCode"
) && !oauth2.auth.code) {
if (!isValid) {
oauth2.errCb({
authId: oauth2.auth.name,
source: "auth",
level: "warning",
message: "Authorization may be unsafe, passed state was changed in server Passed state wasn't returned from auth server"
});
}

if (qp.code) {
delete oauth2.state;
oauth2.auth.code = qp.code;
oauth2.callback({auth: oauth2.auth, redirectUrl: redirectUrl});
} else {
let oauthErrorMsg
if (qp.error) {
oauthErrorMsg = "["+qp.error+"]: " +
(qp.error_description ? qp.error_description+ ". " : "no accessCode received from the server. ") +
(qp.error_uri ? "More info: "+qp.error_uri : "");
}

oauth2.errCb({
authId: oauth2.auth.name,
source: "auth",
level: "error",
message: oauthErrorMsg || "[Authorization failed]: no accessCode received from the server"
});
}
} else {
oauth2.callback({auth: oauth2.auth, token: qp, isValid: isValid, redirectUrl: redirectUrl});
}
window.close();
}
</script>

0 comments on commit 647e83a

Please sign in to comment.