Skip to content

Commit

Permalink
feat: Http endpoint base class with request context access (#18)
Browse files Browse the repository at this point in the history
* feat: Http endpoint base class with request context access

# Conflicts:
#	akka-javasdk/src/main/java/akka/javasdk/http/RequestContext.java
#	akka-javasdk/src/main/scala/akka/javasdk/impl/SdkRunner.scala

* post rebase fixes

* formatting...
  • Loading branch information
johanandren committed Nov 21, 2024
1 parent 6cf5087 commit 1e6842d
Show file tree
Hide file tree
Showing 13 changed files with 119 additions and 91 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import akka.javasdk.annotations.JWT;
import akka.javasdk.annotations.http.HttpEndpoint;
import akka.javasdk.annotations.http.Get;
import akka.javasdk.http.RequestContext;
import akka.javasdk.http.AbstractHttpEndpoint;

import java.util.concurrent.CompletionStage;
import static java.util.concurrent.CompletableFuture.completedStage;
Expand All @@ -19,17 +19,12 @@
// tag::bearer-token[]
@HttpEndpoint("/hello")
@JWT(validate = JWT.JwtMethodMode.BEARER_TOKEN, bearerTokenIssuers = "my-issuer-123", staticClaims = { @JWT.StaticClaim(claim = "sub", pattern = "my-subject-123")})
public class HelloJwtEndpoint {
public class HelloJwtEndpoint extends AbstractHttpEndpoint {
// end::bearer-token[]

RequestContext context;
public HelloJwtEndpoint(RequestContext context){
this.context = context;
}

@Get("/")
public CompletionStage<String> helloWorld() {
var claims = context.getJwtClaims();
var claims = requestContext().getJwtClaims();
var issuer = claims.issuer().get();
var sub = claims.subject().get();
return completedStage("issuer: " + issuer + ", subject: " + sub);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
@HttpEndpoint("/missingjwt")
public class MissingJwtEndpoint {

// Note: leaving this with injected request context rather than extend AbstractHttpEndpoint to keep
// some test coverage
RequestContext context;
public MissingJwtEndpoint(RequestContext context){
this.context = context;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
* <li>{@link io.opentelemetry.api.trace.Span}</li>
* <li>Custom types provided by a {@link akka.javasdk.DependencyProvider} from the service setup</li>
* </ul>
* <p>If the annotated class extends {@link akka.javasdk.http.AbstractHttpEndpoint} the request context
* is available without constructor injection.
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* Copyright (C) 2021-2024 Lightbend Inc. <https://www.lightbend.com>
*/

package akka.javasdk.http;

import akka.annotation.InternalApi;

/**
* Optional base class for HTTP endpoints giving access to a request context without additional constructor parameters
*/
abstract public class AbstractHttpEndpoint {

volatile private RequestContext context;

/**
* INTERNAL API
*
* @hidden
*/
@InternalApi
final public void _internalSetRequestContext(RequestContext context) {
this.context = context;
}

/**
* Always available from request handling methods, not available from the constructor.
*/
protected final RequestContext requestContext() {
if (context == null) {
throw new IllegalStateException("The request context can only be accessed from the request handling methods of the endpoint.");
}
return context;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@
import akka.javasdk.Tracing;

/**
* Not for user extension, can be injected as constructor parameter into HTTP endpoint components
* Not for user extension, can be injected as constructor parameter into HTTP endpoint components or
* accessible from {@link AbstractHttpEndpoint#requestContext()} if the endpoint class extends
* `AbstractHttpEndpoint`.
*/
@DoNotInherit
public interface RequestContext extends Context {
Expand Down
38 changes: 22 additions & 16 deletions akka-javasdk/src/main/scala/akka/javasdk/impl/SdkRunner.scala
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ import akka.javasdk.view.View
import akka.javasdk.workflow.Workflow
import akka.javasdk.workflow.WorkflowContext
import akka.javasdk.JwtClaims
import akka.javasdk.http.AbstractHttpEndpoint
import akka.javasdk.Tracing
import akka.javasdk.impl.http.JwtClaimsImpl
import akka.javasdk.impl.telemetry.SpanTracingImpl
Expand Down Expand Up @@ -560,25 +561,30 @@ private final class Sdk(

private def httpEndpointFactory[E](httpEndpointClass: Class[E]): HttpEndpointConstructionContext => E = {
(context: HttpEndpointConstructionContext) =>
wiredInstance(httpEndpointClass) {
lazy val requestContext = new RequestContext {
override def getPrincipals: Principals =
PrincipalsImpl(context.principal.source, context.principal.service)

override def getJwtClaims: JwtClaims =
context.jwt match {
case Some(jwtClaims) => new JwtClaimsImpl(jwtClaims)
case None =>
throw new RuntimeException(
"There are no JWT claims defined but trying accessing the JWT claims. The class or the method needs to be annotated with @JWT.")
}

override def tracing(): Tracing = new SpanTracingImpl(context.openTelemetrySpan, sdkTracerFactory)
}
val instance = wiredInstance(httpEndpointClass) {
sideEffectingComponentInjects(context.openTelemetrySpan).orElse {
case p if p == classOf[RequestContext] =>
new RequestContext {
override def getPrincipals: Principals =
PrincipalsImpl(context.principal.source, context.principal.service)

override def getJwtClaims: JwtClaims =
context.jwt match {
case Some(jwtClaims) => new JwtClaimsImpl(jwtClaims)
case None =>
throw new RuntimeException(
"There are no JWT claims defined but trying accessing the JWT claims. The class or the method needs to be annotated with @JWT.")
}

override def tracing(): Tracing = new SpanTracingImpl(context.openTelemetrySpan, sdkTracerFactory)
}
case p if p == classOf[RequestContext] => requestContext
}
}
instance match {
case withBaseClass: AbstractHttpEndpoint => withBaseClass._internalSetRequestContext(requestContext)
case _ =>
}
instance
}

private def wiredInstance[T](clz: Class[T])(partial: PartialFunction[Class[_], Any]): T = {
Expand Down
9 changes: 3 additions & 6 deletions docs/src/modules/java/pages/access-control.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -185,19 +185,16 @@ Note that in local development, the services don’t actually authenticate with

== Programmatically accessing principals

The current principal associated with a request can be accessed through the `RequestContext`. The `RequestContext`
can be injected in the endpoint constructor.
The current principal associated with a request can be accessed through the `RequestContext`.

NOTE: Endpoints are stateless and each request is served by a new Endpoint instance. Therefore, the
injected `RequestContext` is always a new instance and is associated with the request currently being handled.
NOTE: Endpoints are stateless and each request is served by a new Endpoint instance. Therefore, the `RequestContext` is always a new instance and is associated with the request currently being handled.

[source, java, indent=0]
.{sample-base-url}/doc-snippets/src/main/java/com/example/acl/UserEndpoint.java[UserEndpoint.java]
----
include::example$doc-snippets/src/main/java/com/example/acl/UserEndpoint.java[tags=endpoint-class;request-context]
----

<1> Inject `RequestContext` on your endpoint constructor to gain access to the request specific context.
<1> Let your endpoint extend link:_attachments/api/akka/javasdk/http/AbstractHttpEndpoint.html[AbstractHttpEndpoint] to get access to the request specific `RequestContext` through `requestContext()`.

You can access the current Principals through method `RequestContext.getPrincipals()`

Expand Down
6 changes: 3 additions & 3 deletions docs/src/modules/java/pages/auth-with-jwts.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -90,15 +90,15 @@ include::example$endpoint-jwt/src/main/java/hellojwt/api/HelloJwtEndpoint.java[t
----

The token extracted from the bearer token must have one of the two issuers defined in the annotation.
Akka will place the claims from the validated token in the link:_attachments/api/akka/javasdk/http/RequestContext.html[RequestContext], so you can access them from your service via `getJwtClaims()`. The `RequestContext` can be injected into the endpoint constructor, so you can retrieve the JWT claims like this:
Akka will place the claims from the validated token in the link:_attachments/api/akka/javasdk/http/RequestContext.html[RequestContext], so you can access them from your service via `getJwtClaims()`. The `RequestContext` is accessed by letting the endpoint extend link:_attachments/api/akka/javasdk/http/AbstractHttpEndpoint.html[AbstractHttpEndpoint] which provides the method `requestContext()`, so you can retrieve the JWT claims like this:

[source, java, indent=0]
.{sample-base-url}/endpoint-jwt/src/main/java/hellojwt/api/HelloJwtEndpoint.java[HelloJwtEndpoint.java]
----
include::example$endpoint-jwt/src/main/java/hellojwt/api/HelloJwtEndpoint.java[tag=accessing-claims]
----
<1> Access the claims in the request. Note that while the `get()` is generally a bad practice, here we know the claims must be present given the `@JWT` configuration.

<1> Access the claims from the request context.
<2> Note that while calling `Optional#get()` is generally a bad practice, here we know the claims must be present given the `@JWT` configuration.


== Running locally with JWT support
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import akka.javasdk.annotations.http.Get;
import akka.javasdk.annotations.http.HttpEndpoint;
import akka.javasdk.annotations.http.Post;
import akka.javasdk.http.RequestContext;
import akka.javasdk.http.AbstractHttpEndpoint;

/*
// tag::class-level-acl[]
Expand All @@ -20,32 +20,25 @@
*/
// tag::endpoint-class[]
@HttpEndpoint("/user")
public class UserEndpoint {
public class UserEndpoint extends AbstractHttpEndpoint { // <1>
// ...
// end::endpoint-class[]

// tag::request-context[]
final private RequestContext requestContext;

public UserEndpoint(RequestContext requestContext) { // <1>
this.requestContext = requestContext;
}
// end::request-context[]

public record CreateUser(String username, String email) { }

// tag::checking-principals[]
@Get
public String checkingPrincipals() {
if (requestContext.getPrincipals().isInternet()) {
var principals = requestContext().getPrincipals();
if (principals.isInternet()) {
return "accessed from the Internet";
} else if (requestContext.getPrincipals().isSelf()) {
} else if (principals.isSelf()) {
return "accessed from Self (internal call from current service)";
} else if (requestContext.getPrincipals().isBackoffice()) {
} else if (principals.isBackoffice()) {
return "accessed from Backoffice API";
} else {
return "accessed from another service: " +
requestContext.getPrincipals().getLocalService();
principals.getLocalService();
}
}
// end::checking-principals[]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import akka.javasdk.annotations.http.HttpEndpoint;
// end::basic-endpoint[]
import akka.javasdk.annotations.http.Post;
import akka.javasdk.http.AbstractHttpEndpoint;
import akka.javasdk.http.HttpException;
import akka.javasdk.http.HttpResponses;
import akka.stream.Materializer;
Expand All @@ -28,7 +29,7 @@
@HttpEndpoint("/example") // <1>
@Acl(allow = @Acl.Matcher(principal = Acl.Principal.ALL)) // <2>
// tag::lower-level-request[]
public class ExampleEndpoint {
public class ExampleEndpoint extends AbstractHttpEndpoint {

// end::basic-endpoint[]
private final Materializer materializer;
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.example.jwt;

import akka.javasdk.annotations.Acl;
import akka.javasdk.annotations.http.HttpEndpoint;
import akka.javasdk.http.AbstractHttpEndpoint;
import akka.javasdk.annotations.JWT;

// tag::bearer-token[]
@HttpEndpoint("/example-jwt") // <1>
@Acl(allow = @Acl.Matcher(principal = Acl.Principal.ALL)) // <2>
@JWT(validate = JWT.JwtMethodMode.BEARER_TOKEN,
bearerTokenIssuers = "my-issuer") // <1>
public class HelloJwtEndpoint extends AbstractHttpEndpoint {

public String message(String msg) {
//..
// end::bearer-token[]
return "ok! Claims: " + String.join(",", requestContext().getJwtClaims().allClaimNames());
// tag::bearer-token[]
}

@JWT(validate = JWT.JwtMethodMode.BEARER_TOKEN,
bearerTokenIssuers = "my-other-issuer")
public String messageWithIssuer(String msg) { // <3>
//..
// end::bearer-token[]
return "ok! Claims: " + String.join(",", requestContext().getJwtClaims().allClaimNames());
// tag::bearer-token[]
}
}
// end::bearer-token[]
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

import akka.javasdk.annotations.http.Get;
// tag::accessing-claims[]
import akka.javasdk.http.RequestContext;
import akka.javasdk.http.AbstractHttpEndpoint;

// end::accessing-claims[]

Expand All @@ -24,16 +24,9 @@
@HttpEndpoint("/hello")
@JWT(validate = JWT.JwtMethodMode.BEARER_TOKEN, bearerTokenIssuers = "my-issuer") // <1>
// tag::accessing-claims[]
public class HelloJwtEndpoint {
public class HelloJwtEndpoint extends AbstractHttpEndpoint {
// end::bearer-token[]
// end::accessing-claims[]
// tag::accessing-claims[]

RequestContext context;
public HelloJwtEndpoint(RequestContext context){
this.context = context;
}
// end::accessing-claims[]

@Get("/")
public CompletionStage<String> hello() {
Expand All @@ -47,9 +40,9 @@ public CompletionStage<String> hello() {
// end::multiple-bearer-token-issuers[]
@Get("/claims")
public CompletionStage<String> helloClaims() {
var claims = context.getJwtClaims();
var issuer = claims.issuer().get(); // <1>
var sub = claims.subject().get(); // <1>
var claims = requestContext().getJwtClaims(); // <1>
var issuer = claims.issuer().get(); // <2>
var sub = claims.subject().get(); // <2>
return completedStage("issuer: " + issuer + ", subject: " + sub);
}
// tag::bearer-token[]
Expand Down

0 comments on commit 1e6842d

Please sign in to comment.