Skip to content

Instantly share code, notes, and snippets.

@sebastianrothbucher
Last active November 11, 2023 18:41
Show Gist options
  • Save sebastianrothbucher/8422e875205c6b21175e7319bcbd4133 to your computer and use it in GitHub Desktop.
Save sebastianrothbucher/8422e875205c6b21175e7319bcbd4133 to your computer and use it in GitHub Desktop.
JAMstack with Keycloak and Boot
// ...
@Autowired
private JwtAuthFilter jwtAuthFilter;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class)
// ...
return http.build();
}
//...
jwt.key=<from keycloak - insert here>
<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>
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);
}
}
<!-- ... -->
<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