-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
14 changed files
with
534 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
= Micronaut Security | ||
|
||
This project showcases how to test secured Micronaut applications. | ||
|
||
We use https://micronaut-projects.github.io/micronaut-security/latest/guide/index.html[Micronaut Security] to do that. | ||
Micronaut Security offers the possibility to secure your application on the HTTP layer. | ||
|
||
E.g. https://spring.io/projects/spring-security[Spring Security] offers additionally to secure you business logic and not just your Web-Controllers (See https://docs.spring.io/spring-security/reference/servlet/authorization/method-security.html#page-title[Method Security]). | ||
If you think the author is mistaken and Micronaut Security offers this as well, do not hesitate to open a bug. | ||
|
||
== What do we showcase? | ||
|
||
We showcase how to use Basic Authentication, which is enabled by default. | ||
|
||
In addition to that we offer JWT based Authentication, too. | ||
|
||
We show two different ways to secure your endpoints: | ||
|
||
* `io.micronaut.security.annotation.Secured` | ||
* https://micronaut-projects.github.io/micronaut-security/latest/guide/#interceptUrlMap[Intercept URL map] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
plugins { | ||
id("cnt-micronaut.micronaut-conventions") | ||
} | ||
|
||
dependencies { | ||
implementation("io.micronaut.security:micronaut-security") | ||
implementation("io.micronaut.security:micronaut-security-jwt") | ||
// We use this to have implementations of org.reactivestreams.Publisher | ||
// which we will use while developing some security related things | ||
implementation("io.micronaut.reactor:micronaut-reactor") | ||
|
||
testImplementation("io.micronaut.test:micronaut-test-rest-assured") | ||
} | ||
|
||
application { | ||
mainClass.set("example.micronaut.security.SecurityApplicationKt") | ||
} |
7 changes: 7 additions & 0 deletions
7
examples/security/src/main/kotlin/example/micronaut/security/SecurityApplicationKt.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
package example.micronaut.security | ||
|
||
import io.micronaut.runtime.Micronaut.run | ||
|
||
fun main(args: Array<String>) { | ||
run(*args) | ||
} |
12 changes: 12 additions & 0 deletions
12
examples/security/src/main/kotlin/example/micronaut/security/api/BookRepresentation.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
package example.micronaut.security.api | ||
|
||
import example.micronaut.security.core.Book | ||
import java.util.UUID | ||
|
||
data class BookRepresentation( | ||
val id: UUID, | ||
val title: String, | ||
val isbn: String | ||
) { | ||
constructor(book: Book) : this(book.id, book.title, book.isbn) | ||
} |
49 changes: 49 additions & 0 deletions
49
examples/security/src/main/kotlin/example/micronaut/security/api/BooksResource.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
package example.micronaut.security.api | ||
|
||
import example.micronaut.security.core.BookCollection | ||
import io.micronaut.http.HttpResponse | ||
import io.micronaut.http.annotation.Body | ||
import io.micronaut.http.annotation.Controller | ||
import io.micronaut.http.annotation.Get | ||
import io.micronaut.http.annotation.Post | ||
import io.micronaut.security.annotation.Secured | ||
import io.micronaut.security.rules.SecurityRule | ||
import jakarta.validation.Valid | ||
|
||
|
||
@Controller("/books") | ||
open class BooksResource(private val bookCollection: BookCollection) { | ||
|
||
@Get | ||
@Secured(SecurityRule.IS_AUTHENTICATED) | ||
open fun findBooks(): Collection<BookRepresentation> { | ||
return findBooksInternal() | ||
} | ||
|
||
@Post | ||
@Secured("BOOK_CREATOR") | ||
open fun createBook(@Valid @Body createBookRequest: CreateBookRequest): HttpResponse<BookRepresentation> { | ||
return createBookInternal(createBookRequest) | ||
} | ||
|
||
// the following two endpoint are secured via configuration of intercept URL map. See | ||
// application.yaml for details. | ||
@Get("/url-not-secured") | ||
open fun findBooksUnsecured(): Collection<BookRepresentation> { | ||
return findBooksInternal() | ||
} | ||
|
||
@Post("/url-not-secured") | ||
open fun createBookUnsecured(@Valid @Body createBookRequest: CreateBookRequest): HttpResponse<BookRepresentation> { | ||
return createBookInternal(createBookRequest) | ||
} | ||
|
||
private fun findBooksInternal(): List<BookRepresentation> { | ||
return bookCollection.getAll().map { BookRepresentation(it) } | ||
} | ||
|
||
private fun createBookInternal(createBookRequest: CreateBookRequest): HttpResponse<BookRepresentation> { | ||
val book = bookCollection.add(createBookRequest.isbn, createBookRequest.title) | ||
return HttpResponse.created(BookRepresentation(book)) | ||
} | ||
} |
11 changes: 11 additions & 0 deletions
11
examples/security/src/main/kotlin/example/micronaut/security/api/CreateBookRequest.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
package example.micronaut.security.api | ||
|
||
import io.micronaut.core.annotation.Introspected | ||
import jakarta.validation.constraints.Pattern | ||
|
||
@Introspected // necessary for jakarta-validation to work at runtime | ||
data class CreateBookRequest( | ||
val title: String, | ||
@field:Pattern(regexp = "[0-9]{13}") | ||
val isbn: String | ||
) |
9 changes: 9 additions & 0 deletions
9
examples/security/src/main/kotlin/example/micronaut/security/core/Book.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
package example.micronaut.security.core | ||
|
||
import java.util.UUID | ||
|
||
data class Book( | ||
val id: UUID, | ||
val isbn: String, | ||
val title: String, | ||
) |
18 changes: 18 additions & 0 deletions
18
examples/security/src/main/kotlin/example/micronaut/security/core/BookCollection.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
package example.micronaut.security.core | ||
|
||
import jakarta.inject.Singleton | ||
import java.util.UUID | ||
|
||
@Singleton | ||
class BookCollection { | ||
|
||
private val books = mutableListOf<Book>() | ||
|
||
fun getAll(): Collection<Book> = books | ||
|
||
fun add(isbn: String, title: String): Book { | ||
val book = Book(UUID.randomUUID(), isbn, title) | ||
books.add(book) | ||
return book | ||
} | ||
} |
38 changes: 38 additions & 0 deletions
38
...urity/src/main/kotlin/example/micronaut/security/infrastructure/AuthenticationProvider.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
package example.micronaut.security.infrastructure | ||
|
||
import io.micronaut.http.HttpRequest | ||
import io.micronaut.security.authentication.AuthenticationProvider | ||
import io.micronaut.security.authentication.AuthenticationRequest | ||
import io.micronaut.security.authentication.AuthenticationResponse | ||
import jakarta.inject.Singleton | ||
import org.reactivestreams.Publisher | ||
import reactor.core.publisher.Mono | ||
|
||
/** | ||
* Provides a simple, in memory AuthenticationProvider that knows two pairs of credentials. | ||
*/ | ||
@Singleton | ||
class AuthenticationProviderUserPassword : AuthenticationProvider<HttpRequest<*>> { | ||
private val knownCredentials = mapOf( | ||
"reader" to "reader's secret", | ||
"writer" to "writer's secret" | ||
) | ||
|
||
private val rightsPerUser = mapOf("writer" to listOf("BOOK_CREATOR")) | ||
|
||
override fun authenticate( | ||
httpRequest: HttpRequest<*>, | ||
authenticationRequest: AuthenticationRequest<*, *> | ||
): Publisher<AuthenticationResponse> { | ||
return Mono.create { | ||
val username = authenticationRequest.identity.toString() | ||
val password = knownCredentials[username] | ||
if (authenticationRequest.secret == password) { | ||
val roles = rightsPerUser[username] ?: listOf() | ||
it.success(AuthenticationResponse.success(username, roles)) | ||
} else { | ||
it.error(AuthenticationResponse.exception()) | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
micronaut: | ||
security: | ||
# this triggers the usage of io.micronaut.security.token.bearer.AccessRefreshTokenLoginHandler | ||
# That class generates a JWT on successful POST to /login | ||
authentication: "bearer" | ||
endpoints: | ||
login: | ||
# enables /login endpoint to be able to request a JWT | ||
enabled: true | ||
token: | ||
jwt: | ||
enabled: true | ||
signatures: | ||
secret: | ||
# this triggers to sign a JWT | ||
generator: | ||
secret: pleaseChangeThisSecretToSomethingThatProvidesAtLeast256Bits | ||
jws-algorithm: HS256 | ||
intercept-url-map: | ||
- pattern: /login | ||
http-method: POST | ||
access: | ||
- isAnonymous() | ||
- pattern: /books/url-not-secured | ||
access: | ||
- isAuthenticated() | ||
- pattern: /books/url-not-secured | ||
http-method: POST | ||
access: | ||
- BOOK_CREATOR | ||
|
121 changes: 121 additions & 0 deletions
121
...les/security/src/test/kotlin/example/micronaut/security/api/BooksResourceBasicAuthTest.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,121 @@ | ||
package example.micronaut.security.api | ||
|
||
import io.micronaut.test.extensions.junit5.annotation.MicronautTest | ||
import io.restassured.http.ContentType | ||
import io.restassured.specification.RequestSpecification | ||
import org.junit.jupiter.api.Nested | ||
import org.junit.jupiter.api.Test | ||
|
||
@MicronautTest | ||
class BooksResourceBasicAuthTest( | ||
private val spec: RequestSpecification | ||
) { | ||
@Nested | ||
inner class SecuredByAnnotationOnController : BookResourceFixture(spec, "/books") | ||
|
||
@Nested | ||
inner class SecuredByInterceptUrlMap : BookResourceFixture(spec, "/books/url-not-secured") | ||
|
||
abstract inner class BookResourceFixture( | ||
private val spec: RequestSpecification, | ||
private val path: String | ||
) { | ||
@Test | ||
fun `returns 401 UNAUTHORIZED if authentication is missing`() { | ||
spec | ||
.get(path) | ||
.then() | ||
.statusCode(401) | ||
} | ||
|
||
@Test | ||
fun `returns 401 UNAUTHORIZED if user name is incorrect`() { | ||
spec | ||
.basicAuth("unknown User", "") | ||
.get(path) | ||
.then() | ||
.statusCode(401) | ||
} | ||
|
||
@Test | ||
fun `returns 401 UNAUTHORIZED if password is incorrect`() { | ||
spec | ||
.basicAuth("reader", "NOT reader's secret") | ||
.get(path) | ||
.then() | ||
.statusCode(401) | ||
} | ||
|
||
@Test | ||
fun `returns 200 OK if authenticated and authorized`() { | ||
spec | ||
.basicAuth("reader", "reader's secret") | ||
.get(path) | ||
.then() | ||
.statusCode(200) | ||
} | ||
|
||
@Test | ||
fun `returns 403 FORBIDDEN if not authorized`() { | ||
spec | ||
.basicAuth("reader", "reader's secret") | ||
.jsonBody( | ||
"""{ | ||
"title": "Some title", | ||
"isbn": "0123456789123" | ||
}""".trimIndent() | ||
) | ||
.post(path) | ||
.then() | ||
.statusCode(403) | ||
} | ||
|
||
@Test | ||
fun `returns 400 BAD REQUEST for a malformed request body`() { | ||
spec | ||
.basicAuth("writer", "writer's secret") | ||
.jsonBody( | ||
"""{ | ||
"a body": "that is invalid" | ||
}""".trimIndent() | ||
) | ||
.post(path) | ||
.then() | ||
.statusCode(400) | ||
} | ||
|
||
@Test | ||
fun `returns 201 CREATED if authorized`() { | ||
spec | ||
.basicAuth("writer", "writer's secret") | ||
.jsonBody( | ||
"""{ | ||
"title": "Some title", | ||
"isbn": "0123456789123" | ||
}""".trimIndent() | ||
) | ||
.post(path) | ||
.then() | ||
.statusCode(201) | ||
} | ||
|
||
private fun RequestSpecification.basicAuth( | ||
username: String, | ||
password: String | ||
): RequestSpecification { | ||
return this | ||
.auth() | ||
// directly sends authorization header to prevent additional round trip | ||
.preemptive() | ||
.basic(username, password) | ||
} | ||
|
||
private fun RequestSpecification.jsonBody(body: String): RequestSpecification { | ||
// contentType is important. | ||
// If you do not specify it, Micronaut Security will not match the request to an endpoint | ||
// and will always return a 403. | ||
return this.contentType(ContentType.JSON) | ||
.body(body) | ||
} | ||
} | ||
} |
Oops, something went wrong.