diff --git a/pom.xml b/pom.xml index 0db6abf..1e5ce75 100644 --- a/pom.xml +++ b/pom.xml @@ -37,6 +37,11 @@ org.springframework.boot + spring-boot-starter-thymeleaf + + + + org.springframework.boot spring-boot-starter-security diff --git a/src/main/java/com/heaerie/server/auth201/Auth201Server/config/OAuth2SecurityConfig.java b/src/main/java/com/heaerie/server/auth201/Auth201Server/config/OAuth2SecurityConfig.java new file mode 100644 index 0000000..b5688de --- /dev/null +++ b/src/main/java/com/heaerie/server/auth201/Auth201Server/config/OAuth2SecurityConfig.java @@ -0,0 +1,254 @@ +package com.heaerie.server.auth201.Auth201Server.config; + +import com.nimbusds.jose.jwk.JWKSet; +import com.nimbusds.jose.jwk.RSAKey; +import com.nimbusds.jose.jwk.source.ImmutableJWKSet; +import com.nimbusds.jose.jwk.source.JWKSource; +import com.nimbusds.jose.proc.SecurityContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.Order; +import org.springframework.security.config.Customizer; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.ClientAuthenticationMethod; +import org.springframework.security.oauth2.core.oidc.OidcScopes; +import org.springframework.security.oauth2.jwt.JwtDecoder; +import org.springframework.security.oauth2.jwt.NimbusJwtDecoder; +import org.springframework.security.oauth2.server.authorization.client.InMemoryRegisteredClientRepository; +import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; +import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository; +import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings; +import org.springframework.security.oauth2.server.authorization.settings.ClientSettings; +import org.springframework.security.oauth2.server.authorization.settings.TokenSettings; +import org.springframework.security.oauth2.server.authorization.token.JwtEncodingContext; +import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenCustomizer; +import org.springframework.security.provisioning.InMemoryUserDetailsManager; +import org.springframework.security.web.SecurityFilterChain; + +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.interfaces.RSAPrivateKey; +import java.security.interfaces.RSAPublicKey; +import java.time.Duration; +import java.util.UUID; + +/** + * OAuth 2.1 Authorization Server Configuration + * Implements modern OAuth 2.1 security standards with PKCE, refresh token rotation, and JWT tokens + */ +@Configuration +@EnableWebSecurity +public class OAuth2SecurityConfig { + + /** + * Authorization Server Security Filter Chain (Order 1) + * Handles all OAuth 2.1 protocol endpoints + */ + @Bean + @Order(1) + public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception { + http + .securityMatcher("/oauth2/**", "/.well-known/**") + .authorizeHttpRequests(authorize -> authorize + .anyRequest().authenticated() + ) + .csrf(csrf -> csrf.ignoringRequestMatchers("/oauth2/**")) + .oauth2AuthorizationServer(Customizer.withDefaults()) + .formLogin(Customizer.withDefaults()); + + return http.build(); + } + + /** + * Default Security Filter Chain (Order 2) + * Handles general authentication and resource protection + */ + @Bean + @Order(2) + public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception { + http + .authorizeHttpRequests(authorize -> authorize + .requestMatchers("/", "/authorized", "/logged-out", "/h2-console/**", "/error").permitAll() + .anyRequest().authenticated() + ) + .formLogin(form -> form + .loginPage("/login") + .permitAll() + ) + .oauth2ResourceServer(oauth2 -> oauth2 + .jwt(Customizer.withDefaults()) + ); + + // H2 Console configuration (development only) + http.csrf(csrf -> csrf.ignoringRequestMatchers("/h2-console/**")); + http.headers(headers -> headers.frameOptions(frame -> frame.sameOrigin())); + + return http.build(); + } + + /** + * Registered Client Repository - OAuth 2.1 compliant clients + */ + @Bean + public RegisteredClientRepository registeredClientRepository(PasswordEncoder passwordEncoder) { + // Public Client (SPAs, Mobile Apps) - OAuth 2.1 + RegisteredClient publicClient = RegisteredClient.withId(UUID.randomUUID().toString()) + .clientId("public-client") + .clientAuthenticationMethod(ClientAuthenticationMethod.NONE) // No secret for public clients + .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) + .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN) + .redirectUri("http://localhost:9000/authorized") + .redirectUri("http://127.0.0.1:8080/authorized") + .redirectUri("http://127.0.0.1:8080/login/oauth2/code/public-client") + .postLogoutRedirectUri("http://localhost:9000/logged-out") + .postLogoutRedirectUri("http://127.0.0.1:8080/logged-out") + .scope(OidcScopes.OPENID) + .scope(OidcScopes.PROFILE) + .scope(OidcScopes.EMAIL) + .scope("read") + .scope("write") + .clientSettings(ClientSettings.builder() + .requireAuthorizationConsent(true) + .requireProofKey(true) // PKCE required (OAuth 2.1) + .build()) + .tokenSettings(TokenSettings.builder() + .accessTokenTimeToLive(Duration.ofMinutes(15)) + .refreshTokenTimeToLive(Duration.ofHours(24)) + .reuseRefreshTokens(false) // Token rotation (OAuth 2.1) + .build()) + .build(); + + // Confidential Client (Server-to-Server) - OAuth 2.1 + RegisteredClient confidentialClient = RegisteredClient.withId(UUID.randomUUID().toString()) + .clientId("confidential-client") + .clientSecret(passwordEncoder.encode("secret")) + .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC) + .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_POST) + .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) + .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN) + .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS) + .redirectUri("http://localhost:9000/authorized") + .redirectUri("http://127.0.0.1:8080/authorized") + .redirectUri("http://127.0.0.1:8080/login/oauth2/code/confidential-client") + .postLogoutRedirectUri("http://localhost:9000/logged-out") + .postLogoutRedirectUri("http://127.0.0.1:8080/logged-out") + .scope(OidcScopes.OPENID) + .scope(OidcScopes.PROFILE) + .scope(OidcScopes.EMAIL) + .scope("read") + .scope("write") + .clientSettings(ClientSettings.builder() + .requireAuthorizationConsent(true) + .requireProofKey(true) // PKCE recommended + .build()) + .tokenSettings(TokenSettings.builder() + .accessTokenTimeToLive(Duration.ofMinutes(15)) + .refreshTokenTimeToLive(Duration.ofHours(24)) + .reuseRefreshTokens(false) // Token rotation (OAuth 2.1) + .build()) + .build(); + + return new InMemoryRegisteredClientRepository(publicClient, confidentialClient); + } + + /** + * User Details Service - Authentication + */ + @Bean + public UserDetailsService userDetailsService(PasswordEncoder passwordEncoder) { + UserDetails user = User.builder() + .username("user") + .password(passwordEncoder.encode("password")) + .roles("USER") + .build(); + + UserDetails admin = User.builder() + .username("admin") + .password(passwordEncoder.encode("admin")) + .roles("USER", "ADMIN") + .build(); + + return new InMemoryUserDetailsManager(user, admin); + } + + /** + * Password Encoder (BCrypt) + */ + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + + /** + * JWK Source for JWT token signing + */ + @Bean + public JWKSource jwkSource() { + KeyPair keyPair = generateRsaKey(); + RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic(); + RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate(); + RSAKey rsaKey = new RSAKey.Builder(publicKey) + .privateKey(privateKey) + .keyID(UUID.randomUUID().toString()) + .build(); + JWKSet jwkSet = new JWKSet(rsaKey); + return new ImmutableJWKSet<>(jwkSet); + } + + /** + * Generate RSA key pair + */ + private static KeyPair generateRsaKey() { + try { + KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA"); + keyPairGenerator.initialize(2048); + return keyPairGenerator.generateKeyPair(); + } catch (Exception ex) { + throw new IllegalStateException("Failed to generate RSA key pair", ex); + } + } + + /** + * JWT Decoder for token validation + */ + @Bean + public JwtDecoder jwtDecoder() { + // Generate RSA key pair for decoding + KeyPair keyPair = generateRsaKey(); + RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic(); + return NimbusJwtDecoder.withPublicKey(publicKey).build(); + } + + /** + * Authorization Server Settings + */ + @Bean + public AuthorizationServerSettings authorizationServerSettings() { + return AuthorizationServerSettings.builder() + .issuer("http://localhost:9000") + .build(); + } + + /** + * JWT Token Customizer for OIDC claims + */ + @Bean + public OAuth2TokenCustomizer tokenCustomizer() { + return context -> { + if (OidcScopes.OPENID.equals(context.getAuthorizedScopes().stream() + .filter(scope -> scope.equals(OidcScopes.OPENID)) + .findFirst() + .orElse(null))) { + // Add OIDC standard claims + context.getClaims().claim("sub", context.getPrincipal().getName()); + } + }; + } +} diff --git a/src/main/java/com/heaerie/server/auth201/Auth201Server/config/SecurityConfig.java b/src/main/java/com/heaerie/server/auth201/Auth201Server/config/SecurityConfig.java deleted file mode 100644 index 4fecc34..0000000 --- a/src/main/java/com/heaerie/server/auth201/Auth201Server/config/SecurityConfig.java +++ /dev/null @@ -1,211 +0,0 @@ -package com.heaerie.server.auth201.Auth201Server.config; - -import com.nimbusds.jose.jwk.JWKSet; -import com.nimbusds.jose.jwk.RSAKey; -import com.nimbusds.jose.jwk.source.ImmutableJWKSet; -import com.nimbusds.jose.jwk.source.JWKSource; -import com.nimbusds.jose.proc.SecurityContext; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.security.config.Customizer; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; -import org.springframework.security.core.userdetails.User; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.security.oauth2.core.AuthorizationGrantType; -import org.springframework.security.oauth2.core.ClientAuthenticationMethod; -import org.springframework.security.oauth2.core.oidc.OidcScopes; -import org.springframework.security.oauth2.jwt.JwtDecoder; -import org.springframework.security.oauth2.jwt.NimbusJwtDecoder; -import org.springframework.security.oauth2.server.authorization.client.InMemoryRegisteredClientRepository; -import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; -import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository; -import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings; -import org.springframework.security.oauth2.server.authorization.settings.ClientSettings; -import org.springframework.security.oauth2.server.authorization.settings.TokenSettings; -import org.springframework.security.provisioning.InMemoryUserDetailsManager; -import org.springframework.security.web.SecurityFilterChain; - -import java.security.KeyPair; -import java.security.KeyPairGenerator; -import java.security.interfaces.RSAPrivateKey; -import java.security.interfaces.RSAPublicKey; -import java.time.Duration; -import java.util.UUID; - -@Configuration -@EnableWebSecurity -public class SecurityConfig { - - /** - * Default Security Filter Chain for authentication - * Handles login and OAuth 2.1 authorization - */ - @Bean - public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { - http - .authorizeHttpRequests(authorize -> authorize - .requestMatchers("/", "/h2-console/**").permitAll() - .anyRequest().authenticated() - ) - .formLogin(Customizer.withDefaults()) - .oauth2ResourceServer(oauth2 -> oauth2 - .jwt(Customizer.withDefaults()) - ); - - // Allow H2 console access (development only) - http.csrf(csrf -> csrf - .ignoringRequestMatchers("/h2-console/**") - ); - http.headers(headers -> headers - .frameOptions(frame -> frame.sameOrigin()) - ); - - return http.build(); - } - - /** - * Registered Client Repository with OAuth 2.1 compliant configuration - * This example uses in-memory storage, but you can implement database storage - */ - @Bean - public RegisteredClientRepository registeredClientRepository() { - // OAuth 2.1 Public Client with PKCE (recommended for SPAs and mobile apps) - RegisteredClient publicClient = RegisteredClient.withId(UUID.randomUUID().toString()) - .clientId("public-client") - // No client secret for public clients (OAuth 2.1 requirement) - .clientAuthenticationMethod(ClientAuthenticationMethod.NONE) - .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) - .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN) - .redirectUri("http://127.0.0.1:8080/login/oauth2/code/public-client") - .redirectUri("http://127.0.0.1:8080/authorized") - .postLogoutRedirectUri("http://127.0.0.1:8080/logged-out") - .scope(OidcScopes.OPENID) - .scope(OidcScopes.PROFILE) - .scope(OidcScopes.EMAIL) - .scope("read") - .scope("write") - .clientSettings(ClientSettings.builder() - .requireAuthorizationConsent(true) - .requireProofKey(true) // PKCE is required (OAuth 2.1) - .build()) - .tokenSettings(TokenSettings.builder() - .accessTokenTimeToLive(Duration.ofMinutes(15)) - .refreshTokenTimeToLive(Duration.ofHours(24)) - .reuseRefreshTokens(false) // OAuth 2.1: Refresh token rotation - .build()) - .build(); - - // OAuth 2.1 Confidential Client (for server-to-server) - RegisteredClient confidentialClient = RegisteredClient.withId(UUID.randomUUID().toString()) - .clientId("confidential-client") - .clientSecret("{bcrypt}$2a$10$MJJhX9K8bGqVHJwF8X8o6O5fF5F5F5F5F5F5F5F5F5F5F5F5F5F5F") // "secret" - .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC) - .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_POST) - .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) - .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN) - .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS) - .redirectUri("http://127.0.0.1:8080/login/oauth2/code/confidential-client") - .redirectUri("http://127.0.0.1:8080/authorized") - .postLogoutRedirectUri("http://127.0.0.1:8080/logged-out") - .scope(OidcScopes.OPENID) - .scope(OidcScopes.PROFILE) - .scope(OidcScopes.EMAIL) - .scope("read") - .scope("write") - .clientSettings(ClientSettings.builder() - .requireAuthorizationConsent(true) - .requireProofKey(true) // PKCE recommended even for confidential clients - .build()) - .tokenSettings(TokenSettings.builder() - .accessTokenTimeToLive(Duration.ofMinutes(15)) - .refreshTokenTimeToLive(Duration.ofHours(24)) - .reuseRefreshTokens(false) // OAuth 2.1: Refresh token rotation - .build()) - .build(); - - return new InMemoryRegisteredClientRepository(publicClient, confidentialClient); - } - - /** - * User Details Service for authentication - * In production, this should be backed by a database - */ - @Bean - public UserDetailsService userDetailsService() { - UserDetails user = User.builder() - .username("user") - .password(passwordEncoder().encode("password")) - .roles("USER") - .build(); - - UserDetails admin = User.builder() - .username("admin") - .password(passwordEncoder().encode("admin")) - .roles("USER", "ADMIN") - .build(); - - return new InMemoryUserDetailsManager(user, admin); - } - - /** - * Password Encoder - */ - @Bean - public PasswordEncoder passwordEncoder() { - return new BCryptPasswordEncoder(); - } - - /** - * JWK Source for signing JWT tokens - */ - @Bean - public JWKSource jwkSource() { - KeyPair keyPair = generateRsaKey(); - RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic(); - RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate(); - RSAKey rsaKey = new RSAKey.Builder(publicKey) - .privateKey(privateKey) - .keyID(UUID.randomUUID().toString()) - .build(); - JWKSet jwkSet = new JWKSet(rsaKey); - return new ImmutableJWKSet<>(jwkSet); - } - - /** - * Generate RSA key pair for JWT signing - */ - private static KeyPair generateRsaKey() { - KeyPair keyPair; - try { - KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA"); - keyPairGenerator.initialize(2048); - keyPair = keyPairGenerator.generateKeyPair(); - } catch (Exception ex) { - throw new IllegalStateException(ex); - } - return keyPair; - } - - /** - * JWT Decoder for validating JWT tokens - */ - @Bean - public JwtDecoder jwtDecoder(JWKSource jwkSource) { - return NimbusJwtDecoder.withJwkSetUri("http://localhost:9000/oauth2/jwks").build(); - } - - /** - * Authorization Server Settings - * Configures the OAuth 2.1 endpoints - */ - @Bean - public AuthorizationServerSettings authorizationServerSettings() { - return AuthorizationServerSettings.builder() - .issuer("http://localhost:9000") - .build(); - } -} diff --git a/src/main/java/com/heaerie/server/auth201/Auth201Server/controller/LoginController.java b/src/main/java/com/heaerie/server/auth201/Auth201Server/controller/LoginController.java new file mode 100644 index 0000000..277c5bb --- /dev/null +++ b/src/main/java/com/heaerie/server/auth201/Auth201Server/controller/LoginController.java @@ -0,0 +1,13 @@ +package com.heaerie.server.auth201.Auth201Server.controller; + +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; + +@Controller +public class LoginController { + + @GetMapping("/login") + public String login() { + return "login"; + } +} diff --git a/src/main/java/com/heaerie/server/auth201/Auth201Server/controller/UserController.java b/src/main/java/com/heaerie/server/auth201/Auth201Server/controller/UserController.java index 8dd08de..a1ce582 100644 --- a/src/main/java/com/heaerie/server/auth201/Auth201Server/controller/UserController.java +++ b/src/main/java/com/heaerie/server/auth201/Auth201Server/controller/UserController.java @@ -2,7 +2,10 @@ import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import java.util.Map; @@ -36,3 +39,42 @@ ); } } + +/** + * OAuth 2.1 Callback Controller + * Handles the authorization callback and displays the result + */ +@Controller +class CallbackController { + + /** + * OAuth 2.1 Authorization Callback Endpoint + * This endpoint receives the authorization code after successful authentication + */ + @GetMapping("/authorized") + public String authorized( + @RequestParam(required = false) String code, + @RequestParam(required = false) String state, + @RequestParam(required = false) String error, + @RequestParam(required = false) String error_description, + Model model + ) { + model.addAttribute("code", code); + model.addAttribute("state", state); + model.addAttribute("error", error); + model.addAttribute("errorDescription", error_description); + model.addAttribute("hasError", error != null); + model.addAttribute("tokenEndpoint", "http://localhost:9000/oauth2/token"); + + return "authorized"; + } + + /** + * Logged out callback endpoint + */ + @GetMapping("/logged-out") + public String loggedOut(Model model) { + model.addAttribute("message", "You have been successfully logged out"); + return "logged-out"; + } +} diff --git a/src/main/resources/static/test-client.html b/src/main/resources/static/test-client.html new file mode 100644 index 0000000..61d9a72 --- /dev/null +++ b/src/main/resources/static/test-client.html @@ -0,0 +1,455 @@ + + + + + + OAuth 2.1 Test Client + + + +
+

πŸ” OAuth 2.1 Test Client

+

Interactive OAuth 2.1 Authorization Code Flow with PKCE

+ +
+

Configuration

+
+
Authorization Server:
+
http://localhost:9000
+ +
Client ID:
+
public-client
+ +
Redirect URI:
+
http://localhost:9000/authorized
+ +
Scopes:
+
profile read write
+ +
PKCE:
+
βœ… Required (S256)
+
+
+ +
+

Step-by-Step Authorization

+ +
+ 1 + Generate PKCE Challenge + +
+
+ +
+ 2 + Start Authorization + +

+ Login credentials: user/password or admin/admin +

+
+ +
+ 3 + Authorization Result +
+
+
+
+ + + + diff --git a/src/main/resources/templates/authorized.html b/src/main/resources/templates/authorized.html new file mode 100644 index 0000000..9c02c61 --- /dev/null +++ b/src/main/resources/templates/authorized.html @@ -0,0 +1,275 @@ + + + + + + OAuth 2.1 Authorization Complete + + + +
+
+
❌
+

Authorization Failed

+

There was an error during the authorization process

+ +
+
+ Error +
error_code
+
+
+ Description +
Error description
+
+
+ + Back to Home +
+ +
+
βœ“
+

Authorization Successful!

+

OAuth 2.1 Authorization Code Flow - Step 1 Complete

+ +
+
+ Authorization Code +
authorization_code_here
+
+
+ State +
state_value
+
+
+ +
+

πŸ“ Next Steps: Exchange Code for Tokens

+
    +
  1. Copy the authorization code from above
  2. +
  3. Use the code verifier from your original PKCE challenge
  4. +
  5. Make a POST request to the token endpoint (see example below)
  6. +
+
+ +
+# Step 2: Exchange authorization code for access token +curl -X POST http://localhost:9000/oauth2/token \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "grant_type=authorization_code" \ + -d "code=YOUR_CODE" \ + -d "redirect_uri=http://127.0.0.1:8080/authorized" \ + -d "client_id=public-client" \ + -d "code_verifier=YOUR_CODE_VERIFIER" + +# For confidential clients, use Basic Auth: +curl -X POST http://localhost:9000/oauth2/token \ + -u confidential-client:secret \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "grant_type=authorization_code" \ + -d "code=YOUR_CODE" \ + -d "redirect_uri=http://127.0.0.1:8080/authorized" \ + -d "code_verifier=YOUR_CODE_VERIFIER" +
+ +
+
+ ⚠️ Important Notes +
    +
  • Authorization codes are single-use and expire quickly (typically 5-10 minutes)
  • +
  • You must use the same redirect_uri as in the authorization request
  • +
  • For public clients, the code_verifier parameter is required (PKCE)
  • +
  • The code verifier must match the code challenge used in the authorization request
  • +
+
+
+ + +
+ + +
+ + diff --git a/src/main/resources/templates/logged-out.html b/src/main/resources/templates/logged-out.html new file mode 100644 index 0000000..c6d9f26 --- /dev/null +++ b/src/main/resources/templates/logged-out.html @@ -0,0 +1,93 @@ + + + + + + Logged Out - OAuth 2.1 Server + + + +
+
πŸ‘‹
+

Logged Out Successfully

+

You have been successfully logged out

+ Return to Home + +
+ + diff --git a/src/main/resources/templates/login.html b/src/main/resources/templates/login.html new file mode 100644 index 0000000..01c8075 --- /dev/null +++ b/src/main/resources/templates/login.html @@ -0,0 +1,382 @@ + + + + + + Sign in - Heaerie SSO + + + + + + + + diff --git a/test-oauth.sh b/test-oauth.sh index dad62aa..b788651 100755 --- a/test-oauth.sh +++ b/test-oauth.sh @@ -7,16 +7,23 @@ CLIENT_ID="public-client" CONFIDENTIAL_CLIENT_ID="confidential-client" CONFIDENTIAL_CLIENT_SECRET="secret" -REDIRECT_URI="http://127.0.0.1:8080/authorized" +REDIRECT_URI="http://localhost:9000/authorized" echo "================================" echo "OAuth 2.1 Authorization Server" echo "================================" echo "" -# 1. Test Well-Known Configuration -echo "1. Fetching Well-Known Configuration..." -curl -s "$BASE_URL/.well-known/oauth-authorization-server" | jq '.' || echo "Server not running or jq not installed" +# 1. Test Server Status +echo "1. Testing Server Status..." +SERVER_STATUS=$(curl -s "$BASE_URL/") +if [ $? -eq 0 ]; then + echo " βœ“ Server is running on $BASE_URL" + echo " Response: $SERVER_STATUS" +else + echo " βœ— Server is not running. Please start it with: ./mvnw spring-boot:run" + exit 1 +fi echo "" # 2. Generate PKCE Challenge @@ -37,6 +44,9 @@ echo " After approval, you'll be redirected to:" echo " $REDIRECT_URI?code=AUTHORIZATION_CODE&state=xyz123" echo "" +echo " NOTE: The authorization code will be displayed on the callback page." +echo " Copy it from the 'Authorization Code' field on the page." +echo "" # 4. Prompt for Authorization Code read -p "4. Enter the authorization code from redirect: " AUTH_CODE @@ -53,16 +63,31 @@ -d "client_id=$CLIENT_ID" \ -d "code_verifier=$CODE_VERIFIER") - echo "$TOKEN_RESPONSE" | jq '.' || echo "$TOKEN_RESPONSE" + if command -v jq &> /dev/null; then + echo "$TOKEN_RESPONSE" | jq '.' + else + echo "$TOKEN_RESPONSE" + fi - ACCESS_TOKEN=$(echo "$TOKEN_RESPONSE" | jq -r '.access_token') - REFRESH_TOKEN=$(echo "$TOKEN_RESPONSE" | jq -r '.refresh_token') + if command -v jq &> /dev/null; then + ACCESS_TOKEN=$(echo "$TOKEN_RESPONSE" | jq -r '.access_token') + REFRESH_TOKEN=$(echo "$TOKEN_RESPONSE" | jq -r '.refresh_token') + else + echo " Note: Install 'jq' for better JSON formatting" + ACCESS_TOKEN=$(echo "$TOKEN_RESPONSE" | grep -o '"access_token":"[^"]*"' | cut -d'"' -f4) + REFRESH_TOKEN=$(echo "$TOKEN_RESPONSE" | grep -o '"refresh_token":"[^"]*"' | cut -d'"' -f4) + fi echo "" # 6. Test UserInfo Endpoint if [ "$ACCESS_TOKEN" != "null" ] && [ -n "$ACCESS_TOKEN" ]; then echo "6. Fetching UserInfo..." - curl -s -H "Authorization: Bearer $ACCESS_TOKEN" "$BASE_URL/userinfo" | jq '.' || echo "Failed to fetch userinfo" + USERINFO=$(curl -s -H "Authorization: Bearer $ACCESS_TOKEN" "$BASE_URL/userinfo") + if command -v jq &> /dev/null; then + echo "$USERINFO" | jq '.' + else + echo "$USERINFO" + fi echo "" # 7. Test Refresh Token @@ -74,10 +99,17 @@ -d "refresh_token=$REFRESH_TOKEN" \ -d "client_id=$CLIENT_ID") - echo "$REFRESH_RESPONSE" | jq '.' || echo "$REFRESH_RESPONSE" + if command -v jq &> /dev/null; then + echo "$REFRESH_RESPONSE" | jq '.' + else + echo "$REFRESH_RESPONSE" + fi echo "" fi fi +else + echo " Skipping token exchange (no authorization code provided)" + echo "" fi # 8. Test Client Credentials Flow @@ -88,9 +120,26 @@ -d "grant_type=client_credentials" \ -d "scope=read write") -echo "$CLIENT_CREDS_RESPONSE" | jq '.' || echo "$CLIENT_CREDS_RESPONSE" +if command -v jq &> /dev/null; then + echo "$CLIENT_CREDS_RESPONSE" | jq '.' +else + echo "$CLIENT_CREDS_RESPONSE" +fi echo "" echo "================================" -echo "Testing Complete!" +echo "βœ“ Testing Complete!" echo "================================" +echo "" +echo "Summary:" +echo " - Server: Running on $BASE_URL" +echo " - Callback URL: $REDIRECT_URI" +echo " - Public Client: $CLIENT_ID" +echo " - Confidential Client: $CONFIDENTIAL_CLIENT_ID" +echo "" +echo "Next steps:" +echo " 1. Open the authorization URL in your browser" +echo " 2. Login with user/password or admin/admin" +echo " 3. Copy the authorization code from the callback page" +echo " 4. Run this script again and paste the code when prompted" +echo ""