Skip to content

Commit

Permalink
Examples of Micronaut Security
Browse files Browse the repository at this point in the history
  • Loading branch information
quandor committed Oct 18, 2023
1 parent 25efbcc commit 4274757
Show file tree
Hide file tree
Showing 14 changed files with 534 additions and 0 deletions.
20 changes: 20 additions & 0 deletions examples/security/README.adoc
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]
17 changes: 17 additions & 0 deletions examples/security/build.gradle.kts
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")
}
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)
}
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)
}
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))
}
}
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
)
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,
)
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
}
}
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())
}
}
}
}
31 changes: 31 additions & 0 deletions examples/security/src/main/resources/application.yaml
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

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)
}
}
}
Loading

0 comments on commit 4274757

Please sign in to comment.