Enable creating an API using Spring Web #5

Merged
niat8586 merged 39 commits from spring into develop 2024-11-06 11:23:29 +01:00
6 changed files with 142 additions and 1 deletions
Showing only changes of commit 15d427fcf8 - Show all commits

85
GetToken.java Normal file
View File

@ -0,0 +1,85 @@
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpServer;
import java.io.IOException;
import java.io.OutputStream;
import java.net.Authenticator;
import java.net.InetSocketAddress;
import java.net.PasswordAuthentication;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public class GetToken {
public static void main(String[] args) throws IOException {
URI baseUri = URI.create("http://localhost:59733");
String clientId = "get-token";
String clientSecret = "get-token-secret";
System.out.println("Browse to " + baseUri.resolve("authorize?response_type=code&client_id=" + clientId));
HttpClient httpClient = HttpClient.newBuilder()
.authenticator(new Authenticator() {
@Override
protected PasswordAuthentication getPasswordAuthentication() {
return new PasswordAuthentication(clientId, clientSecret.toCharArray());
}
})
.build();
HttpServer httpServer = HttpServer.create();
httpServer.bind(new InetSocketAddress(59732), 0);
Thread thread = Thread.currentThread();
httpServer.createContext("/", exchange -> {
exchange.sendResponseHeaders(200, 0);
try (OutputStream responseBody = exchange.getResponseBody()) {
responseBody.write("All done, close tab".getBytes(StandardCharsets.UTF_8));
}
Map<String, List<String>> queryParams = getQueryParams(exchange);
String code = queryParams.get("code").get(0);
HttpRequest httpRequest = HttpRequest.newBuilder()
.uri(baseUri.resolve("exchange"))
.header("Content-Type", "application/x-www-form-urlencoded")
.POST(HttpRequest.BodyPublishers.ofString("grant_type=authorization_code&code=" + code))
.build();
try {
HttpResponse<String> response = httpClient.send(httpRequest, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
} catch (Exception e) {
e.printStackTrace();
} finally {
thread.interrupt();
}
});
httpServer.start();
try {
Thread.sleep(Duration.ofMinutes(1L).toMillis());
System.out.println("No authorization within one minute, exiting.");
System.exit(1);
} catch (InterruptedException ignored) {
// expected
}
httpServer.stop(0);
}
private static Map<String, List<String>> getQueryParams(final HttpExchange exchange) {
String query = exchange.getRequestURI()
.getQuery();
return Arrays.stream(query.split("&"))
.map(s -> s.split("="))
.collect(Collectors.groupingBy(
split -> split[0],
Collectors.mapping(split -> split[1], Collectors.toList())));
}
}

13
README.md Normal file
View File

@ -0,0 +1,13 @@
## Working with the API
The API is protected by OAuth 2 acting as a [resource server](https://www.oauth.com/oauth2-servers/the-resource-server/)
verifying tokens using [token introspection](https://datatracker.ietf.org/doc/html/rfc7662).
When developing it uses a locally running instance of an
[authorization server](https://datatracker.ietf.org/doc/html/rfc6749#section-1.1)
that is run inside [Docker](https://www.docker.com). It can be started with `docker compose -f docker-compose.yml up`.
Since there is no frontend to interact with the authorization server there's a helper script in
[GetToken.java](GetToken.java) that can be run directly with `java GetToken.java` to run through the authorization flow
and get an access token.
Once the token has been obtained go to the [Swagger UI](http://localhost:8080/api/swagger) to interact with the API.
Click the "Authorize" button in the top right and paste the access token to log in.

15
docker-compose.yml Normal file
View File

@ -0,0 +1,15 @@
services:
oauth2:
container_name: scipro-dev-oauth2
build:
context: https://github.com/dsv-su/toker.git
dockerfile: embedded.Dockerfile
restart: on-failure
ports:
- '59733:8080'
environment:
- CLIENT_ID=get-token
- CLIENT_SECRET=get-token-secret
- CLIENT_REDIRECT_URI=http://localhost:59732/
- RESOURCE_SERVER_ID=scipro-api-client
- RESOURCE_SERVER_SECRET=scipro-api-secret

View File

@ -32,6 +32,10 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>

View File

@ -1,5 +1,9 @@
package se.su.dsv.scipro.war;
import io.swagger.v3.oas.models.security.SecurityRequirement;
import io.swagger.v3.oas.models.security.SecurityScheme;
import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI;
import org.springframework.boot.autoconfigure.web.servlet.WebMvcProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
@ -11,6 +15,8 @@ import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher;
import org.springframework.web.servlet.handler.HandlerMappingIntrospector;
import java.util.List;
@Configuration(proxyBeanMethods = false)
@ComponentScan("se.su.dsv.scipro.api")
public class ApiConfig {
@ -48,7 +54,19 @@ public class ApiConfig {
.securityMatcher(mvc.pattern("/**"))
.authorizeHttpRequests(authorize -> authorize
.anyRequest().authenticated())
.httpBasic(Customizer.withDefaults())
.oauth2ResourceServer(oauth2 -> oauth2.opaqueToken(Customizer.withDefaults()))
.build();
}
@Bean
public OpenAPI customOpenAPI() {
return new OpenAPI()
.security(List.of(new SecurityRequirement().addList("access-token")))
.components(new Components()
.addSecuritySchemes("access-token", new SecurityScheme()
.type(SecurityScheme.Type.HTTP)
.in(SecurityScheme.In.HEADER)
.scheme("bearer")
.bearerFormat("opaque")));
}
}

View File

@ -12,3 +12,9 @@ spring.jpa.hibernate.naming.implicit-strategy=org.hibernate.boot.model.naming.Im
spring.mvc.servlet.path=/api
springdoc.swagger-ui.path=/swagger
springdoc.swagger-ui.persist-authorization=true
# These will be overwritten by configuration in the environment of servers it is deployed to
spring.security.oauth2.resourceserver.opaquetoken.client-id=scipro-api-client
spring.security.oauth2.resourceserver.opaquetoken.client-secret=scipro-api-secret
spring.security.oauth2.resourceserver.opaquetoken.introspection-uri=http://localhost:59733/introspect