diff --git a/guides/micronaut-oauth2-oidc-microsoft/java/src/main/java/example/micronaut/HomeController.java b/guides/micronaut-oauth2-oidc-microsoft/java/src/main/java/example/micronaut/HomeController.java new file mode 100644 index 0000000000..aac777bbbd --- /dev/null +++ b/guides/micronaut-oauth2-oidc-microsoft/java/src/main/java/example/micronaut/HomeController.java @@ -0,0 +1,39 @@ +/* + * Copyright 2017-2024 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package example.micronaut; + +import io.micronaut.http.MediaType; +import io.micronaut.http.annotation.Controller; +import io.micronaut.http.annotation.Get; +import io.micronaut.http.annotation.Produces; +import io.micronaut.security.annotation.Secured; +import io.micronaut.security.rules.SecurityRule; +import io.micronaut.views.View; + +import java.util.HashMap; +import java.util.Map; + +@Controller // <1> +public class HomeController { + + @Produces(MediaType.TEXT_HTML) + @Secured(SecurityRule.IS_ANONYMOUS) // <2> + @View("home") // <3> + @Get // <4> + public Map index() { + return new HashMap<>(); + } +} \ No newline at end of file diff --git a/guides/micronaut-oauth2-oidc-microsoft/java/src/main/java/example/micronaut/IdTokenClaimsValidatorReplacement.java b/guides/micronaut-oauth2-oidc-microsoft/java/src/main/java/example/micronaut/IdTokenClaimsValidatorReplacement.java new file mode 100644 index 0000000000..676ec04f01 --- /dev/null +++ b/guides/micronaut-oauth2-oidc-microsoft/java/src/main/java/example/micronaut/IdTokenClaimsValidatorReplacement.java @@ -0,0 +1,24 @@ +package example.micronaut; + +import io.micronaut.context.annotation.Replaces; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.security.oauth2.client.IdTokenClaimsValidator; +import io.micronaut.security.oauth2.configuration.OauthClientConfiguration; +import io.micronaut.security.oauth2.configuration.OpenIdClientConfiguration; +import jakarta.inject.Singleton; + +import java.util.Collection; +import java.util.Optional; + +@Replaces(IdTokenClaimsValidator.class) +@Singleton +class IdTokenClaimsValidatorReplacement extends IdTokenClaimsValidator { + public IdTokenClaimsValidatorReplacement(Collection oauthClientConfigurations) { + super(oauthClientConfigurations); + } + + @Override + protected @NonNull Optional matchesIssuer(@NonNull OpenIdClientConfiguration openIdClientConfiguration, @NonNull String iss) { + return Optional.of(Boolean.TRUE); + } +} diff --git a/guides/micronaut-oauth2-oidc-microsoft/java/src/test/java/example/micronaut/HomeControllerAnonymousTest.java b/guides/micronaut-oauth2-oidc-microsoft/java/src/test/java/example/micronaut/HomeControllerAnonymousTest.java new file mode 100644 index 0000000000..7e6995eb83 --- /dev/null +++ b/guides/micronaut-oauth2-oidc-microsoft/java/src/test/java/example/micronaut/HomeControllerAnonymousTest.java @@ -0,0 +1,23 @@ +package example.micronaut; + +import io.micronaut.http.HttpRequest; +import io.micronaut.http.MediaType; +import io.micronaut.http.client.BlockingHttpClient; +import io.micronaut.http.client.HttpClient; +import io.micronaut.http.client.annotation.Client; +import io.micronaut.test.extensions.junit5.annotation.MicronautTest; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +@MicronautTest +class HomeControllerAnonymousTest { + + @Test + void homeRendersHtmlPage(@Client("/")HttpClient httpClient) { + BlockingHttpClient client = httpClient.toBlocking(); + String html = assertDoesNotThrow(() -> client.retrieve(HttpRequest.GET("/").accept(MediaType.TEXT_HTML))); + assertTrue(html.contains("")); + assertTrue(html.contains("Anonymous")); + } +} \ No newline at end of file diff --git a/guides/micronaut-oauth2-oidc-microsoft/java/src/test/java/example/micronaut/HomeControllerAuthenticatedTest.java b/guides/micronaut-oauth2-oidc-microsoft/java/src/test/java/example/micronaut/HomeControllerAuthenticatedTest.java new file mode 100644 index 0000000000..42294f01e8 --- /dev/null +++ b/guides/micronaut-oauth2-oidc-microsoft/java/src/test/java/example/micronaut/HomeControllerAuthenticatedTest.java @@ -0,0 +1,45 @@ +package example.micronaut; + +import io.micronaut.context.annotation.Property; +import io.micronaut.context.annotation.Requires; +import io.micronaut.core.async.publisher.Publishers; +import io.micronaut.http.HttpRequest; +import io.micronaut.http.MediaType; +import io.micronaut.http.client.BlockingHttpClient; +import io.micronaut.http.client.HttpClient; +import io.micronaut.http.client.annotation.Client; +import io.micronaut.security.authentication.Authentication; +import io.micronaut.security.filters.AuthenticationFetcher; +import io.micronaut.test.extensions.junit5.annotation.MicronautTest; +import jakarta.inject.Singleton; +import org.junit.jupiter.api.Test; +import org.reactivestreams.Publisher; + +import java.security.PublicKey; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; + +@Property(name = "spec.name", value = "HomeControllerAuthenticatedTest") +@MicronautTest +class HomeControllerAuthenticatedTest { + + @Test + void homeRendersHtmlPage(@Client("/")HttpClient httpClient) { + BlockingHttpClient client = httpClient.toBlocking(); + String html = assertDoesNotThrow(() -> client.retrieve(HttpRequest.GET("/").accept(MediaType.TEXT_HTML))); + assertTrue(html.contains("")); + assertFalse(html.contains("Anonymous")); + assertTrue(html.contains("sergio.delamo@softamo.com")); + } + + @Requires(property = "spec.name", value = "HomeControllerAuthenticatedTest") + @Singleton + static class MockAuthenticationFetcher implements AuthenticationFetcher> { + + @Override + public Publisher fetchAuthentication(HttpRequest request) { + return Publishers.just(Authentication.build("sdelamo", Map.of("email", "sergio.delamo@softamo.com"))); + } + } +} \ No newline at end of file diff --git a/guides/micronaut-oauth2-oidc-microsoft/metadata.json b/guides/micronaut-oauth2-oidc-microsoft/metadata.json new file mode 100644 index 0000000000..0867c9fe3f --- /dev/null +++ b/guides/micronaut-oauth2-oidc-microsoft/metadata.json @@ -0,0 +1,15 @@ +{ + "title": "Secure a Micronaut application with Microsoft Identify Platform", + "intro": "Learn how to create a Micronaut application and secure it with Microsoft Identity Platform and provide authentication with OpenID Connect", + "authors": ["Sergio del Amo"], + "tags": ["oauth2", "microsoft", "oidc", "security"], + "categories": ["Authorization Code"], + "publicationDate": "2024-09-11", + "languages": ["java"], + "apps": [ + { + "name": "default", + "features": ["graalvm", "views-thymeleaf", "security-jwt", "security-oauth2"] + } + ] +} diff --git a/guides/micronaut-oauth2-oidc-microsoft/micronaut-oauth2-oidc-microsoft.adoc b/guides/micronaut-oauth2-oidc-microsoft/micronaut-oauth2-oidc-microsoft.adoc new file mode 100644 index 0000000000..97f2c49f50 --- /dev/null +++ b/guides/micronaut-oauth2-oidc-microsoft/micronaut-oauth2-oidc-microsoft.adoc @@ -0,0 +1,139 @@ +common:header.adoc[] + +common:requirements.adoc[] + +common:completesolution.adoc[] + +== OAuth 2.0 + +- Sign in to the https://entra.microsoft.com/[Microsoft Entra admin center]. +- Browse to `Identity > Applications > App registrations` +- New Registration + +image::microsoft-identity-platform/microsoft-identity-platform-new-registration.png[] + +- As supported account types select "Accounts in any organizational directory (Any Azure AD directory - Multitenant) and personal Microsoft accounts (e.g. Skype, Xbox)" +- Set as redirect URI `Web: http://localhost:8080/oauth/callback/microsoft` + +image::microsoft-identity-platform/microsoft-identity-platform-1.png[] + +=== Client ID + +- Annotate the `Application (client) ID`. You will use it as the OAuth 2 Client ID in the Micronaut application. + +image::microsoft-identity-platform/microsoft-identity-platform-client-id.png[] + +=== Client Secret + +Create a client secret: + +image::microsoft-identity-platform/microsoft-identity-platform-2.png[] + +- Annotate the secret value. You will use it as the OAuth 2 Client Secret in the Micronaut application. + +image::microsoft-identity-platform/microsoft-identity-platform-client-secret.png[] + +common:create-app.adoc[] + +=== Dependencies + +common:micronaut-views-thymeleaf.adoc[] + +To use OAuth 2.0 integration in your Micronaut application, add the following dependency: + +dependency:micronaut-security-oauth2[groupId=io.micronaut.security] + +Also add https://micronaut-projects.github.io/micronaut-security/latest/guide/#jwt[Micronaut JWT support] dependencies: + +dependency:micronaut-security-jwt[groupId=io.micronaut.security] + +=== Configuration + +Add the following OAuth2 Configuration: + +resource:application.properties[tag=oauth2] + +callout:authentication-idtoken[number=1,arg0=Microsoft] +<2> You can choose any name. The name you select, will be used in your routes. E.g. If you set `microsoft` the login route for this OAuth 2.0 client is `/oauth/login/microsoft` +<3> Client Secret. See previous screenshot. +<4> Client ID. See previous screenshot. +<5> `issuer` URL. It allows the Micronaut framework to discover the configuration of the OpenID Connect server. +<6> Accept GET request to the `/logout` endpoint. +<7> Disable issuer claim validation + +The previous configuration uses several placeholders. You will need to set up `OAUTH_CLIENT_ID`, `OAUTH_CLIENT_SECRET` environment variables. + +[soruce, bash] +---- +export OAUTH_CLIENT_ID=XXXXXXXXXX +export OAUTH_CLIENT_SECRET=YYYYYYYYYY +---- + +Check Microsoft https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration[.well-known/openid-configuration documentation]. + + +=== Disable issuer claim validation + +The received ID token issuer does not match `https://login.microsoftonline.com/common/v2.0` as the issuer. You can disable the default issuer claim validation via a bean replacement: + +source:IdTokenClaimsValidatorReplacement[] + +and the following configuration setting: + +[source, properties] +---- +micronaut.security.oauth2.openid.claims-validation.issuer=false +---- + +=== Authorization Code Flow + +We want to use an **Authorization Code** grant type flow which it is described in the following diagram: + +image::diagramm.png[] + +=== Controller + +Create a controller to handle the requests to `/`. You will display the email of the authenticated person if any. Annotate the controller endpoint with `@View` since we will use a Thymeleaf template. + +source:HomeController[] + +callout:controller[number=1,arg0=/] +callout:secured-anonymous[number=2] +<3> Use https://micronaut-projects.github.io/micronaut-views/latest/api/io/micronaut/views/View.html[View] annotation to specify which template to use to render the response. +<4> The @api@/io/micronaut/http/annotation/Get.html[@Get] annotation maps the `index` method to GET `/` requests. + +=== View + +Create a thymeleaf template: + +resource:views/home.html[] + +Also, note that we return an empty model in the controller. However, we are accessing `security` in the thymeleaf template. + +- The https://micronaut-projects.github.io/micronaut-views/latest/api/io/micronaut/views/model/security/SecurityViewModelProcessor.html[SecurityViewModelProcessor^] injects into the model a `security` map with the authenticated user. See https://micronaut-projects.github.io/micronaut-views/latest/guide/#security-model-enhancement[User in a view] documentation. + +common:runapp.adoc[] + +image::microsoft-identity-platform/microsoftvideo.gif[] + +== GraalVM Reflection Metadata + +Thymleaf accesses several classes via reflection. + +common:reflect-config-json.adoc[] + +resource:META-INF/native-image/example.micronaut.micronautguide/reflect-config.json[] + +common:graal-with-plugins.adoc[] + +:exclude-for-languages:groovy + +After you execute the native executable, navigate to localhost:8080 and authenticate with Microsoft. + +:exclude-for-languages: + +== Next steps + +Read https://micronaut-projects.github.io/micronaut-security/latest/guide/#oauth[Micronaut OAuth 2.0 documentation] to learn more. + +common:helpWithMicronaut.adoc[] diff --git a/guides/micronaut-oauth2-oidc-microsoft/src/main/resources/META-INF/native-image/example.micronaut.micronautguide/reflect-config.json b/guides/micronaut-oauth2-oidc-microsoft/src/main/resources/META-INF/native-image/example.micronaut.micronautguide/reflect-config.json new file mode 100644 index 0000000000..30dcbe8184 --- /dev/null +++ b/guides/micronaut-oauth2-oidc-microsoft/src/main/resources/META-INF/native-image/example.micronaut.micronautguide/reflect-config.json @@ -0,0 +1,19 @@ +[ + { + "name": "java.util.Collections$UnmodifiableMap", + "queryAllDeclaredMethods": true + }, + { + "name": "java.util.Map", + "queryAllDeclaredMethods": true, + "queryAllPublicMethods": true, + "methods": [ + { + "name": "get", + "parameterTypes": [ + "java.lang.Object" + ] + } + ] + } +] \ No newline at end of file diff --git a/guides/micronaut-oauth2-oidc-microsoft/src/main/resources/application.properties b/guides/micronaut-oauth2-oidc-microsoft/src/main/resources/application.properties new file mode 100644 index 0000000000..742c467b90 --- /dev/null +++ b/guides/micronaut-oauth2-oidc-microsoft/src/main/resources/application.properties @@ -0,0 +1,16 @@ +micronaut.application.name: micronautguide +#tag::oauth2[] +# <1> +micronaut.security.authentication=idtoken +# <2> +# <3> +micronaut.security.oauth2.clients.microsoft.client-secret=${OAUTH_CLIENT_SECRET:yyy} +# <4> +micronaut.security.oauth2.clients.microsoft.client-id=${OAUTH_CLIENT_ID:xxx} +# <5> +micronaut.security.oauth2.clients.microsoft.openid.issuer=https://login.microsoftonline.com/common/v2.0 +# <6> +micronaut.security.endpoints.logout.get-allowed=true +# <7> +micronaut.security.oauth2.openid.claims-validation.issuer=false +#end::oauth2[] diff --git a/guides/micronaut-oauth2-oidc-microsoft/src/main/resources/views/home.html b/guides/micronaut-oauth2-oidc-microsoft/src/main/resources/views/home.html new file mode 100644 index 0000000000..40ebc2b076 --- /dev/null +++ b/guides/micronaut-oauth2-oidc-microsoft/src/main/resources/views/home.html @@ -0,0 +1,19 @@ + + + + Home + + +

Micronaut - Microsoft example

+ +

username:

+

username: Anonymous

+ + + + \ No newline at end of file diff --git a/src/docs/images/microsoft-identity-platform/microsoft-identity-platform-1.png b/src/docs/images/microsoft-identity-platform/microsoft-identity-platform-1.png new file mode 100644 index 0000000000..84a4159023 Binary files /dev/null and b/src/docs/images/microsoft-identity-platform/microsoft-identity-platform-1.png differ diff --git a/src/docs/images/microsoft-identity-platform/microsoft-identity-platform-2.png b/src/docs/images/microsoft-identity-platform/microsoft-identity-platform-2.png new file mode 100644 index 0000000000..4ce5220467 Binary files /dev/null and b/src/docs/images/microsoft-identity-platform/microsoft-identity-platform-2.png differ diff --git a/src/docs/images/microsoft-identity-platform/microsoft-identity-platform-client-id.png b/src/docs/images/microsoft-identity-platform/microsoft-identity-platform-client-id.png new file mode 100644 index 0000000000..b463cc6818 Binary files /dev/null and b/src/docs/images/microsoft-identity-platform/microsoft-identity-platform-client-id.png differ diff --git a/src/docs/images/microsoft-identity-platform/microsoft-identity-platform-client-secret.png b/src/docs/images/microsoft-identity-platform/microsoft-identity-platform-client-secret.png new file mode 100644 index 0000000000..9f54b23e61 Binary files /dev/null and b/src/docs/images/microsoft-identity-platform/microsoft-identity-platform-client-secret.png differ diff --git a/src/docs/images/microsoft-identity-platform/microsoft-identity-platform-new-registration.png b/src/docs/images/microsoft-identity-platform/microsoft-identity-platform-new-registration.png new file mode 100644 index 0000000000..95a98b15bf Binary files /dev/null and b/src/docs/images/microsoft-identity-platform/microsoft-identity-platform-new-registration.png differ diff --git a/src/docs/images/microsoft-identity-platform/microsoftvideo.gif b/src/docs/images/microsoft-identity-platform/microsoftvideo.gif new file mode 100644 index 0000000000..2e735de78b Binary files /dev/null and b/src/docs/images/microsoft-identity-platform/microsoftvideo.gif differ