Last active
November 11, 2023 18:41
-
-
Save sebastianrothbucher/8422e875205c6b21175e7319bcbd4133 to your computer and use it in GitHub Desktop.
JAMstack with Keycloak and Boot
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ... | |
@Autowired | |
private JwtAuthFilter jwtAuthFilter; | |
@Bean | |
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { | |
http | |
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class) | |
// ... | |
return http.build(); | |
} | |
//... |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
jwt.key=<from keycloak - insert here> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<html> | |
<head> | |
<title>OAuth Authorization Code + PKCE in Vanilla JS</title> | |
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no"> | |
</head> | |
<body> | |
<div class="flex-center full-height"> | |
<div class="content"> | |
<a href="#" id="start">Click to Sign In</a> - | |
<a href="#" id="req">Click to fire off a request</a> | |
<div id="token" class="hidden"> | |
<h2>Access Token</h2> | |
<div id="access_token" class="code"></div> | |
<pre id="access_token_details"></pre> | |
<h2>Refresh Token</h2> | |
<div id="refresh_token" class="code"></div> | |
</div> | |
<div id="error" class="hidden"> | |
<h2>Error</h2> | |
<div id="error_details" class="code"></div> | |
</div> | |
</div> | |
</div> | |
<script src="https://cdn.jsdelivr.net/npm/@badgateway/[email protected]/browser/oauth2-client.min.js" integrity="sha256-Zpw3IBP4vjX14/QEhebWiNV7JXgu3sF6dQsaYVBx890=" crossorigin="anonymous"></script> | |
<script> | |
// great article: https://developer.okta.com/blog/2019/05/01/is-the-oauth-implicit-flow-dead | |
// great lib: https://www.npmjs.com/package/@badgateway/oauth2-client (didn't dig in yet, though) | |
const client = new OAuth2Client.OAuth2Client({ | |
server: 'http://localhost:8090/realms/test/protocol/openid-connect/', // slash counts! | |
clientId: 'test', | |
authorizationEndpoint: 'auth', | |
tokenEndpoint: 'token', | |
}); | |
const redirectUri = 'http://localhost:8081/'; | |
// Initiate the PKCE Auth Code flow when the link is clicked | |
document.getElementById("start").addEventListener("click", async function(e) { | |
const codeVerifier = await OAuth2Client.generateCodeVerifier(); | |
localStorage.setItem("pkce_code_verifier", codeVerifier); | |
document.location = await client.authorizationCode.getAuthorizeUri({ | |
redirectUri, | |
codeVerifier, | |
scope: ['openid'], | |
}); | |
}); | |
async function handleRedirectBack() { | |
if (!location.search?.includes('code')) { | |
return; | |
} | |
const codeVerifier = localStorage.getItem("pkce_code_verifier"); | |
let oauth2Token | |
try { | |
oauth2Token = await client.authorizationCode.getTokenFromCodeRedirect( | |
document.location, | |
{ | |
redirectUri, | |
codeVerifier, | |
} | |
); | |
} catch (error) { | |
alert("Error returned from authorization server: " + error); | |
document.getElementById("error_details").innerText = error; | |
document.getElementById("error").classList = ""; | |
return; | |
} | |
localStorage.removeItem("pkce_code_verifier"); | |
window.history.replaceState({}, null, "/"); | |
displayToken(oauth2Token); // now we have the token | |
} | |
handleRedirectBack(); | |
function displayToken(oauth2Token) { | |
document.getElementById("access_token").innerText = oauth2Token.accessToken; | |
const token = JSON.parse(atob(oauth2Token.accessToken.split('.')[1])) | |
document.getElementById("access_token_details").innerText = JSON.stringify(token, null, ' '); | |
document.getElementById("access_token_details").innerText += '\n\n'; | |
document.getElementById("access_token_details").innerText += new Date(token.exp * 1000); | |
document.getElementById("refresh_token").innerText = oauth2Token.refreshToken; | |
document.getElementById("start").classList = "hidden"; | |
document.getElementById("token").classList = ""; | |
} | |
document.getElementById("req").addEventListener("click", async function(e) { | |
const fetchWrapper = new OAuth2Client.OAuth2Fetch({ | |
client, | |
getNewToken: async () => { | |
const newOauth2Token = await client.refreshToken({refreshToken: document.getElementById("refresh_token").innerText}); | |
displayToken(newOauth2Token); | |
return newOauth2Token; | |
}, | |
onError: (error) => alert("Error returned from authorization server: " + error), | |
}); | |
try { | |
const res = await fetchWrapper.fetch('http://localhost:8080/api/test/hey'); | |
const body = await res.text(); | |
alert(body); | |
} catch (e) { | |
console.error(e); | |
alert(e); | |
} | |
}); | |
</script> | |
</body> | |
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package whatever; | |
import io.jsonwebtoken.Claims; | |
import io.jsonwebtoken.ExpiredJwtException; | |
import io.jsonwebtoken.Jwt; | |
import io.jsonwebtoken.Jwts; | |
import jakarta.servlet.FilterChain; | |
import jakarta.servlet.ServletException; | |
import jakarta.servlet.http.HttpServletRequest; | |
import jakarta.servlet.http.HttpServletResponse; | |
import org.slf4j.Logger; | |
import org.slf4j.LoggerFactory; | |
import org.springframework.beans.factory.annotation.Value; | |
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; | |
import org.springframework.security.core.authority.SimpleGrantedAuthority; | |
import org.springframework.security.core.context.SecurityContextHolder; | |
import org.springframework.stereotype.Component; | |
import org.springframework.web.filter.OncePerRequestFilter; | |
import java.io.IOException; | |
import java.io.PrintWriter; | |
import java.security.KeyFactory; | |
import java.security.NoSuchAlgorithmException; | |
import java.security.PublicKey; | |
import java.security.spec.InvalidKeySpecException; | |
import java.security.spec.X509EncodedKeySpec; | |
import java.util.Base64; | |
import java.util.List; | |
@Component | |
public class JwtAuthFilter extends OncePerRequestFilter { | |
static final Logger LOGGER = LoggerFactory.getLogger(JwtAuthFilter.class); | |
@Value("${jwt.key}") | |
private String jwtKey = null; | |
private PublicKey key = null; | |
private boolean keyInitialized = false; | |
@Override | |
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { | |
if (!keyInitialized) { | |
keyInitialized = true; | |
if (jwtKey != null) { | |
final byte[] keyBytes = Base64.getDecoder().decode(jwtKey); | |
final X509EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes); | |
try { | |
key = KeyFactory.getInstance("RSA").generatePublic(keySpec); | |
} catch (InvalidKeySpecException | NoSuchAlgorithmException e) { | |
throw new RuntimeException(e); | |
} | |
} else { | |
LOGGER.warn("JWT key not set - will ignore JWT"); | |
key = null; | |
} | |
} | |
if (key != null && request.getHeader("Authorization") != null && request.getHeader("Authorization").startsWith("Bearer ")){ | |
String token = request.getHeader("Authorization").substring("Bearer ".length()); | |
final Jwt parsed; | |
try { | |
parsed = Jwts.parser() | |
.verifyWith(key) | |
.clockSkewSeconds(10) | |
.build() | |
.parse(token); | |
} catch (ExpiredJwtException e) { | |
response.setStatus(401); | |
try (PrintWriter w = response.getWriter()) { | |
w.write("Token expired"); | |
} | |
return; // we're done | |
} | |
Claims claims = (Claims) parsed.getPayload(); | |
SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken(claims.get("preferred_username", String.class), null, List.of(new SimpleGrantedAuthority("ROLE_USER")))); | |
} | |
filterChain.doFilter(request, response); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!-- ... --> | |
<dependency> | |
<groupId>io.jsonwebtoken</groupId> | |
<artifactId>jjwt-api</artifactId> | |
<version>0.12.3</version> | |
</dependency> | |
<dependency> | |
<groupId>io.jsonwebtoken</groupId> | |
<artifactId>jjwt-impl</artifactId> | |
<version>0.12.3</version> | |
</dependency> | |
<dependency> | |
<groupId>io.jsonwebtoken</groupId> | |
<artifactId>jjwt-jackson</artifactId> | |
<version>0.12.3</version> | |
</dependency> | |
<!-- ... --> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment