Enable creating an API using Spring Web #5
85
GetToken.java
Normal file
85
GetToken.java
Normal 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
13
README.md
Normal 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
15
docker-compose.yml
Normal 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
|
@ -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>
|
||||
|
@ -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")));
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user