diff --git a/examples/README.md b/examples/README.md
index 9664942b5fa..b51d560d7bb 100644
--- a/examples/README.md
+++ b/examples/README.md
@@ -205,6 +205,8 @@ $ bazel-bin/hello-world-client
- [JWT-based Authentication](example-jwt-auth)
+- [OAuth2-based Authentication](example-oauth)
+
- [Pre-serialized messages](src/main/java/io/grpc/examples/preserialized)
## Unit test examples
diff --git a/examples/example-oauth/README.md b/examples/example-oauth/README.md
new file mode 100644
index 00000000000..26a6e223f60
--- /dev/null
+++ b/examples/example-oauth/README.md
@@ -0,0 +1,73 @@
+Authentication Example
+==============================================
+
+This example illustrates a simple OAuth2-based authentication implementation in gRPC using
+ server interceptor. It uses the Google OAuth2 library since it already has the OAuth2
+semantics which makes it easy to illustrate the OAuth2 flow. The example creates an OAuth2
+credentials using the library and converts it to gRPC CallCredentials. However, you may
+use your own OAuth2 implementation, so use of Google OAuth2 library is not necessary.
+
+The example requires grpc-java to be pre-built. Using a release tag will download the relevant binaries
+from a maven repository. But if you need the latest SNAPSHOT binaries you will need to follow
+[COMPILING](../../COMPILING.md) to build these.
+
+The source code is [here](src/main/java/io/grpc/examples/oauth).
+To build the example, run in this directory:
+```
+$ ../gradlew installDist
+```
+The build creates scripts `auth-server` and `auth-client` in the `build/install/example-oauth/bin/` directory
+which can be used to run this example. The example requires the server to be running before starting the
+client.
+
+Running auth-server is similar to the normal hello world example and there are no arguments to supply:
+
+**auth-server**:
+
+The auth-server accepts optional argument for port on which the server should run:
+
+```text
+USAGE: auth-server [port]
+```
+
+The auth-client accepts optional arguments for server-host, server-port, user-name and client-id:
+
+**auth-client**:
+
+```text
+USAGE: auth-client [server-host [server-port [user-name [client-id]]]]
+```
+
+The `user-name` value is simply passed in the `HelloRequest` message as payload and the value of
+`client-id` is included in the OAuth2 access token passed in the metadata header.
+
+
+#### How to run the example:
+
+```bash
+# Run the server:
+./build/install/example-oauth/bin/auth-server 50051
+# In another terminal run the client
+./build/install/example-oauth/bin/auth-client localhost 50051 userA clientB
+```
+
+That's it! The client will show the user-name reflected back in the message from the server as follows:
+```
+INFO: Greeting: Hello, userA
+```
+
+And on the server side you will see the message with the client's identifier:
+```
+Processing request from clientB
+```
+
+## Maven
+
+If you prefer to use Maven follow these [steps](../README.md#maven). You can run the example as follows:
+
+```
+$ # Run the server
+$ mvn exec:java -Dexec.mainClass=io.grpc.examples.oauth.AuthServer -Dexec.args="50051"
+$ # In another terminal run the client
+$ mvn exec:java -Dexec.mainClass=io.grpc.examples.oauth.AuthClient -Dexec.args="localhost 50051 userA clientB"
+```
diff --git a/examples/example-oauth/build.gradle b/examples/example-oauth/build.gradle
new file mode 100644
index 00000000000..ffbe8a53617
--- /dev/null
+++ b/examples/example-oauth/build.gradle
@@ -0,0 +1,87 @@
+plugins {
+ // Provide convenience executables for trying out the examples.
+ id 'application'
+ id 'com.google.protobuf' version '0.9.4'
+ // Generate IntelliJ IDEA's .idea & .iml project files
+ id 'idea'
+}
+
+repositories {
+ maven { // The google mirror is less flaky than mavenCentral()
+ url "https://maven-central.storage-download.googleapis.com/maven2/"
+ }
+ mavenLocal()
+}
+
+java {
+ sourceCompatibility = JavaVersion.VERSION_1_8
+ targetCompatibility = JavaVersion.VERSION_1_8
+}
+
+// IMPORTANT: You probably want the non-SNAPSHOT version of gRPC. Make sure you
+// are looking at a tagged version of the example and not "master"!
+
+// Feel free to delete the comment at the next line. It is just for safely
+// updating the version in our release process.
+def grpcVersion = '1.59.0-SNAPSHOT' // CURRENT_GRPC_VERSION
+def protobufVersion = '3.24.0'
+def protocVersion = protobufVersion
+
+dependencies {
+ implementation "io.grpc:grpc-protobuf:${grpcVersion}"
+ implementation "io.grpc:grpc-stub:${grpcVersion}"
+ implementation "io.grpc:grpc-auth:${grpcVersion}"
+ implementation "com.google.auth:google-auth-library-oauth2-http:1.18.0"
+
+ compileOnly "org.apache.tomcat:annotations-api:6.0.53"
+
+ runtimeOnly "io.grpc:grpc-netty-shaded:${grpcVersion}"
+
+ testImplementation "io.grpc:grpc-testing:${grpcVersion}"
+ testImplementation "junit:junit:4.13.2"
+ testImplementation "org.mockito:mockito-core:3.4.0"
+}
+
+protobuf {
+ protoc { artifact = "com.google.protobuf:protoc:${protocVersion}" }
+ plugins {
+ grpc { artifact = "io.grpc:protoc-gen-grpc-java:${grpcVersion}" }
+ }
+ generateProtoTasks {
+ all()*.plugins { grpc {} }
+ }
+}
+
+// Inform IDEs like IntelliJ IDEA, Eclipse or NetBeans about the generated code.
+sourceSets {
+ main {
+ java {
+ srcDirs 'build/generated/source/proto/main/grpc'
+ srcDirs 'build/generated/source/proto/main/java'
+ }
+ }
+}
+
+startScripts.enabled = false
+
+task hellowWorldOauthServer(type: CreateStartScripts) {
+ mainClass = 'io.grpc.examples.oauth.AuthServer'
+ applicationName = 'auth-server'
+ outputDir = new File(project.buildDir, 'tmp/scripts/' + name)
+ classpath = startScripts.classpath
+}
+
+task hellowWorldOauthClient(type: CreateStartScripts) {
+ mainClass = 'io.grpc.examples.oauth.AuthClient'
+ applicationName = 'auth-client'
+ outputDir = new File(project.buildDir, 'tmp/scripts/' + name)
+ classpath = startScripts.classpath
+}
+
+application {
+ applicationDistribution.into('bin') {
+ from(hellowWorldOauthServer)
+ from(hellowWorldOauthClient)
+ fileMode = 0755
+ }
+}
diff --git a/examples/example-oauth/gradle/wrapper/gradle-wrapper.jar b/examples/example-oauth/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 00000000000..249e5832f09
Binary files /dev/null and b/examples/example-oauth/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/examples/example-oauth/gradle/wrapper/gradle-wrapper.properties b/examples/example-oauth/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 00000000000..ae04661ee73
--- /dev/null
+++ b/examples/example-oauth/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,5 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/examples/example-oauth/gradlew b/examples/example-oauth/gradlew
new file mode 100755
index 00000000000..a69d9cb6c20
--- /dev/null
+++ b/examples/example-oauth/gradlew
@@ -0,0 +1,240 @@
+#!/bin/sh
+
+#
+# Copyright © 2015-2021 the 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.
+#
+
+##############################################################################
+#
+# Gradle start up script for POSIX generated by Gradle.
+#
+# Important for running:
+#
+# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
+# noncompliant, but you have some other compliant shell such as ksh or
+# bash, then to run this script, type that shell name before the whole
+# command line, like:
+#
+# ksh Gradle
+#
+# Busybox and similar reduced shells will NOT work, because this script
+# requires all of these POSIX shell features:
+# * functions;
+# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
+# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
+# * compound commands having a testable exit status, especially «case»;
+# * various built-in commands including «command», «set», and «ulimit».
+#
+# Important for patching:
+#
+# (2) This script targets any POSIX shell, so it avoids extensions provided
+# by Bash, Ksh, etc; in particular arrays are avoided.
+#
+# The "traditional" practice of packing multiple parameters into a
+# space-separated string is a well documented source of bugs and security
+# problems, so this is (mostly) avoided, by progressively accumulating
+# options in "$@", and eventually passing that to Java.
+#
+# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
+# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
+# see the in-line comments for details.
+#
+# There are tweaks for specific operating systems such as AIX, CygWin,
+# Darwin, MinGW, and NonStop.
+#
+# (3) This script is generated from the Groovy template
+# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+# within the Gradle project.
+#
+# You can find Gradle at https://github.com/gradle/gradle/.
+#
+##############################################################################
+
+# Attempt to set APP_HOME
+
+# Resolve links: $0 may be a link
+app_path=$0
+
+# Need this for daisy-chained symlinks.
+while
+ APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
+ [ -h "$app_path" ]
+do
+ ls=$( ls -ld "$app_path" )
+ link=${ls#*' -> '}
+ case $link in #(
+ /*) app_path=$link ;; #(
+ *) app_path=$APP_HOME$link ;;
+ esac
+done
+
+APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
+
+APP_NAME="Gradle"
+APP_BASE_NAME=${0##*/}
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD=maximum
+
+warn () {
+ echo "$*"
+} >&2
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+} >&2
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "$( uname )" in #(
+ CYGWIN* ) cygwin=true ;; #(
+ Darwin* ) darwin=true ;; #(
+ MSYS* | MINGW* ) msys=true ;; #(
+ NONSTOP* ) nonstop=true ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD=$JAVA_HOME/jre/sh/java
+ else
+ JAVACMD=$JAVA_HOME/bin/java
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD=java
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
+ case $MAX_FD in #(
+ max*)
+ MAX_FD=$( ulimit -H -n ) ||
+ warn "Could not query maximum file descriptor limit"
+ esac
+ case $MAX_FD in #(
+ '' | soft) :;; #(
+ *)
+ ulimit -n "$MAX_FD" ||
+ warn "Could not set maximum file descriptor limit to $MAX_FD"
+ esac
+fi
+
+# Collect all arguments for the java command, stacking in reverse order:
+# * args from the command line
+# * the main class name
+# * -classpath
+# * -D...appname settings
+# * --module-path (only if needed)
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if "$cygwin" || "$msys" ; then
+ APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
+ CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
+
+ JAVACMD=$( cygpath --unix "$JAVACMD" )
+
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ for arg do
+ if
+ case $arg in #(
+ -*) false ;; # don't mess with options #(
+ /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
+ [ -e "$t" ] ;; #(
+ *) false ;;
+ esac
+ then
+ arg=$( cygpath --path --ignore --mixed "$arg" )
+ fi
+ # Roll the args list around exactly as many times as the number of
+ # args, so each arg winds up back in the position where it started, but
+ # possibly modified.
+ #
+ # NB: a `for` loop captures its iteration list before it begins, so
+ # changing the positional parameters here affects neither the number of
+ # iterations, nor the values presented in `arg`.
+ shift # remove old arg
+ set -- "$@" "$arg" # push replacement arg
+ done
+fi
+
+# Collect all arguments for the java command;
+# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
+# shell script including quotes and variable substitutions, so put them in
+# double quotes to make sure that they get re-expanded; and
+# * put everything else in single quotes, so that it's not re-expanded.
+
+set -- \
+ "-Dorg.gradle.appname=$APP_BASE_NAME" \
+ -classpath "$CLASSPATH" \
+ org.gradle.wrapper.GradleWrapperMain \
+ "$@"
+
+# Stop when "xargs" is not available.
+if ! command -v xargs >/dev/null 2>&1
+then
+ die "xargs is not available"
+fi
+
+# Use "xargs" to parse quoted args.
+#
+# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
+#
+# In Bash we could simply go:
+#
+# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
+# set -- "${ARGS[@]}" "$@"
+#
+# but POSIX shell has neither arrays nor command substitution, so instead we
+# post-process each arg (as a line of input to sed) to backslash-escape any
+# character that might be a shell metacharacter, then use eval to reverse
+# that process (while maintaining the separation between arguments), and wrap
+# the whole thing up as a single "set" statement.
+#
+# This will of course break if any of these variables contains a newline or
+# an unmatched quote.
+#
+
+eval "set -- $(
+ printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
+ xargs -n1 |
+ sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
+ tr '\n' ' '
+ )" '"$@"'
+
+exec "$JAVACMD" "$@"
diff --git a/examples/example-oauth/gradlew.bat b/examples/example-oauth/gradlew.bat
new file mode 100644
index 00000000000..53a6b238d41
--- /dev/null
+++ b/examples/example-oauth/gradlew.bat
@@ -0,0 +1,91 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@if "%DEBUG%"=="" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%"=="" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if %ERRORLEVEL% equ 0 goto execute
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if %ERRORLEVEL% equ 0 goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+set EXIT_CODE=%ERRORLEVEL%
+if %EXIT_CODE% equ 0 set EXIT_CODE=1
+if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
+exit /b %EXIT_CODE%
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/examples/example-oauth/pom.xml b/examples/example-oauth/pom.xml
new file mode 100644
index 00000000000..2812166e589
--- /dev/null
+++ b/examples/example-oauth/pom.xml
@@ -0,0 +1,141 @@
+
+ 4.0.0
+ io.grpc
+ example-oauth
+ jar
+
+ 1.59.0-SNAPSHOT
+ example-oauth
+ https://github.com/grpc/grpc-java
+
+
+ UTF-8
+ 1.59.0-SNAPSHOT
+ 3.24.0
+ 3.24.0
+
+ 1.8
+ 1.8
+
+
+
+
+
+ io.grpc
+ grpc-bom
+ ${grpc.version}
+ pom
+ import
+
+
+
+
+
+
+ io.grpc
+ grpc-netty-shaded
+ runtime
+
+
+ io.grpc
+ grpc-protobuf
+
+
+ io.grpc
+ grpc-stub
+
+
+ io.grpc
+ grpc-auth
+
+
+ com.google.auth
+ google-auth-library-credentials
+
+
+
+
+ com.google.auth
+ google-auth-library-oauth2-http
+ 1.18.0
+
+
+ org.apache.tomcat
+ annotations-api
+ 6.0.53
+ provided
+
+
+ io.grpc
+ grpc-testing
+ test
+
+
+ junit
+ junit
+ 4.13.2
+ test
+
+
+ org.mockito
+ mockito-core
+ 3.4.0
+ test
+
+
+
+
+
+
+ kr.motd.maven
+ os-maven-plugin
+ 1.7.1
+
+
+
+
+ org.xolstice.maven.plugins
+ protobuf-maven-plugin
+ 0.5.1
+
+
+ com.google.protobuf:protoc:${protoc.version}:exe:${os.detected.classifier}
+
+ grpc-java
+
+ io.grpc:protoc-gen-grpc-java:${grpc.version}:exe:${os.detected.classifier}
+
+
+
+
+
+ compile
+ compile-custom
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-enforcer-plugin
+ 1.4.1
+
+
+ enforce
+
+ enforce
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/example-oauth/settings.gradle b/examples/example-oauth/settings.gradle
new file mode 100644
index 00000000000..273558dd9cf
--- /dev/null
+++ b/examples/example-oauth/settings.gradle
@@ -0,0 +1,8 @@
+pluginManagement {
+ repositories {
+ maven { // The google mirror is less flaky than mavenCentral()
+ url "https://maven-central.storage-download.googleapis.com/maven2/"
+ }
+ gradlePluginPortal()
+ }
+}
diff --git a/examples/example-oauth/src/main/java/io/grpc/examples/oauth/AuthClient.java b/examples/example-oauth/src/main/java/io/grpc/examples/oauth/AuthClient.java
new file mode 100644
index 00000000000..a43f6fbd98e
--- /dev/null
+++ b/examples/example-oauth/src/main/java/io/grpc/examples/oauth/AuthClient.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright 2023 The gRPC 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
+ *
+ * http://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 io.grpc.examples.oauth;
+
+import io.grpc.CallCredentials;
+import io.grpc.Grpc;
+import io.grpc.InsecureChannelCredentials;
+import io.grpc.ManagedChannel;
+import io.grpc.examples.helloworld.GreeterGrpc;
+import io.grpc.examples.helloworld.HelloReply;
+import io.grpc.examples.helloworld.HelloRequest;
+import io.grpc.auth.MoreCallCredentials;
+import java.util.concurrent.TimeUnit;
+import java.util.logging.Logger;
+
+/**
+ * An authenticating client that requests a greeting from the {@link AuthServer}.
+ */
+public class AuthClient {
+
+ private static final Logger logger = Logger.getLogger(AuthClient.class.getName());
+
+ private final ManagedChannel channel;
+ private final GreeterGrpc.GreeterBlockingStub blockingStub;
+ private final CallCredentials callCredentials;
+
+ /**
+ * Construct client for accessing GreeterGrpc server.
+ */
+ AuthClient(CallCredentials callCredentials, String host, int port) {
+ this(
+ callCredentials,
+ // For this example we use plaintext to avoid needing certificates, but it is
+ // recommended to use TlsChannelCredentials.
+ Grpc.newChannelBuilderForAddress(host, port, InsecureChannelCredentials.create())
+ .build());
+ }
+
+ /**
+ * Construct a client for accessing GreeterGrpc server using an existing channel.
+ */
+ AuthClient(CallCredentials callCredentials, ManagedChannel channel) {
+ this.callCredentials = callCredentials;
+ this.channel = channel;
+ this.blockingStub = GreeterGrpc.newBlockingStub(channel);
+ }
+
+ public void shutdown() throws InterruptedException {
+ channel.shutdown().awaitTermination(5, TimeUnit.SECONDS);
+ }
+
+ /**
+ * Say hello to server.
+ *
+ * @param name name to set in HelloRequest
+ * @return the message in the HelloReply from the server
+ */
+ public String greet(String name) {
+ logger.info("Will try to greet " + name + " ...");
+ HelloRequest request = HelloRequest.newBuilder().setName(name).build();
+
+ // Use a stub with the given call credentials applied to invoke the RPC.
+ HelloReply response =
+ blockingStub
+ .withCallCredentials(callCredentials) // callCredentials
+ .sayHello(request);
+
+ logger.info("Greeting: " + response.getMessage());
+ return response.getMessage();
+ }
+
+ private static CallCredentials getOauthCred(String clientId) {
+ ExampleOAuth2Credentials oAuth2Credentials = new ExampleOAuth2Credentials(clientId);
+ return MoreCallCredentials.from(oAuth2Credentials);
+ }
+
+ /**
+ * Greet server. If provided, the first element of {@code args} is the name to use in the greeting
+ * and the second is the client identifier to set in JWT
+ */
+ public static void main(String[] args) throws Exception {
+
+ String host = "localhost";
+ int port = 50051;
+ String user = "world";
+ String clientId = "default-client";
+
+ if (args.length > 0) {
+ host = args[0]; // Use the arg as the server host if provided
+ }
+ if (args.length > 1) {
+ port = Integer.parseInt(args[1]); // Use the second argument as the server port if provided
+ }
+ if (args.length > 2) {
+ user = args[2]; // Use the the third argument as the name to greet if provided
+ }
+ if (args.length > 3) {
+ clientId = args[3]; // Use the fourth argument as the client identifier if provided
+ }
+
+ CallCredentials credentials = getOauthCred(clientId);
+ AuthClient client = new AuthClient(credentials, host, port);
+
+ try {
+ client.greet(user);
+ } finally {
+ client.shutdown();
+ }
+ }
+}
diff --git a/examples/example-oauth/src/main/java/io/grpc/examples/oauth/AuthServer.java b/examples/example-oauth/src/main/java/io/grpc/examples/oauth/AuthServer.java
new file mode 100644
index 00000000000..c44d55476da
--- /dev/null
+++ b/examples/example-oauth/src/main/java/io/grpc/examples/oauth/AuthServer.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2023 The gRPC 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
+ *
+ * http://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 io.grpc.examples.oauth;
+
+import io.grpc.Grpc;
+import io.grpc.InsecureServerCredentials;
+import io.grpc.Server;
+import io.grpc.examples.helloworld.GreeterGrpc;
+import io.grpc.examples.helloworld.HelloReply;
+import io.grpc.examples.helloworld.HelloRequest;
+import io.grpc.stub.StreamObserver;
+import java.io.IOException;
+import java.util.logging.Logger;
+
+/**
+ * Server that manages startup/shutdown of a {@code Greeter} server. This also uses a
+ * {@link OAuth2ServerInterceptor} to intercept the OAuth2 token passed.
+ */
+public class AuthServer {
+
+ private static final Logger logger = Logger.getLogger(AuthServer.class.getName());
+
+ private Server server;
+ private int port;
+
+ public AuthServer(int port) {
+ this.port = port;
+ }
+
+ private void start() throws IOException {
+ server = Grpc.newServerBuilderForPort(port, InsecureServerCredentials.create())
+ .addService(new GreeterImpl())
+ .intercept(new OAuth2ServerInterceptor())
+ .build()
+ .start();
+ logger.info("Server started, listening on " + port);
+ Runtime.getRuntime().addShutdownHook(new Thread() {
+ @Override
+ public void run() {
+ // Use stderr here since the logger may have been reset by its JVM shutdown hook.
+ System.err.println("*** shutting down gRPC server since JVM is shutting down");
+ AuthServer.this.stop();
+ System.err.println("*** server shut down");
+ }
+ });
+ }
+
+ private void stop() {
+ if (server != null) {
+ server.shutdown();
+ }
+ }
+
+ /**
+ * Await termination on the main thread since the grpc library uses daemon threads.
+ */
+ private void blockUntilShutdown() throws InterruptedException {
+ if (server != null) {
+ server.awaitTermination();
+ }
+ }
+
+ /**
+ * Main launches the server from the command line.
+ */
+ public static void main(String[] args) throws IOException, InterruptedException {
+
+ // The port on which the server should run
+ int port = 50051; // default
+ if (args.length > 0) {
+ port = Integer.parseInt(args[0]);
+ }
+
+ final AuthServer server = new AuthServer(port);
+ server.start();
+ server.blockUntilShutdown();
+ }
+
+ static class GreeterImpl extends GreeterGrpc.GreeterImplBase {
+ @Override
+ public void sayHello(HelloRequest req, StreamObserver responseObserver) {
+ // get client id added to context by interceptor
+ String clientId = Constant.CLIENT_ID_CONTEXT_KEY.get();
+ logger.info("Processing request from " + clientId);
+ HelloReply reply = HelloReply.newBuilder().setMessage("Hello, " + req.getName()).build();
+ responseObserver.onNext(reply);
+ responseObserver.onCompleted();
+ }
+ }
+}
diff --git a/examples/example-oauth/src/main/java/io/grpc/examples/oauth/Constant.java b/examples/example-oauth/src/main/java/io/grpc/examples/oauth/Constant.java
new file mode 100644
index 00000000000..1db9b6e4e47
--- /dev/null
+++ b/examples/example-oauth/src/main/java/io/grpc/examples/oauth/Constant.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2023 The gRPC 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
+ *
+ * http://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 io.grpc.examples.oauth;
+
+import static io.grpc.Metadata.ASCII_STRING_MARSHALLER;
+
+import io.grpc.Context;
+import io.grpc.Metadata;
+
+/**
+ * Constants definition
+ */
+final class Constant {
+
+ static final String REFRESH_SUFFIX = "+1";
+ static final String ACCESS_TOKEN = "access-token";
+ static final Context.Key CLIENT_ID_CONTEXT_KEY = Context.key("clientId");
+ static final Metadata.Key AUTHORIZATION_METADATA_KEY = Metadata.Key.of("Authorization", ASCII_STRING_MARSHALLER);
+
+ private Constant() {
+ }
+}
diff --git a/examples/example-oauth/src/main/java/io/grpc/examples/oauth/ExampleOAuth2Credentials.java b/examples/example-oauth/src/main/java/io/grpc/examples/oauth/ExampleOAuth2Credentials.java
new file mode 100644
index 00000000000..e2171ea20f4
--- /dev/null
+++ b/examples/example-oauth/src/main/java/io/grpc/examples/oauth/ExampleOAuth2Credentials.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2023 The gRPC 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
+ *
+ * http://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 io.grpc.examples.oauth;
+
+import com.google.auth.oauth2.AccessToken;
+import com.google.auth.oauth2.OAuth2Credentials;
+import java.io.IOException;
+import java.time.Instant;
+import java.util.Date;
+
+/**
+ * Subclass of {@link OAuth2Credentials } with a simple implementation of
+ * {@link OAuth2Credentials#refreshAccessToken()}. A real implementation
+ * will maintain a refresh token and use it to exchange it for a new
+ * access token from the authorization server.
+ */
+public class ExampleOAuth2Credentials extends OAuth2Credentials {
+
+ /**
+ * Creates an access token using the passed in clientId. A real
+ * implementation will contact the authorization server to get an access
+ * token and a refresh token.
+ *
+ */
+ public ExampleOAuth2Credentials(String clientId) {
+ super(new AccessToken(Constant.ACCESS_TOKEN + ":" + clientId,
+ new Date()));
+ }
+
+
+ /**
+ * Refreshes access token by simply appending ":+1" to the previous value.
+ * A real implementation will use the existing refresh token to get
+ * fresh access and refresh tokens from the authorization server.
+ */
+ @Override
+ public AccessToken refreshAccessToken() throws IOException {
+ AccessToken accessToken = getAccessToken();
+ if (accessToken == null) {
+ throw new IOException("No existing token found");
+ }
+ String tokenValue = accessToken.getTokenValue();
+ return new AccessToken(tokenValue + ":" + Constant.REFRESH_SUFFIX,
+ Date.from((Instant.now().plusSeconds(120))));
+ }
+}
diff --git a/examples/example-oauth/src/main/java/io/grpc/examples/oauth/OAuth2ServerInterceptor.java b/examples/example-oauth/src/main/java/io/grpc/examples/oauth/OAuth2ServerInterceptor.java
new file mode 100644
index 00000000000..032c9297de4
--- /dev/null
+++ b/examples/example-oauth/src/main/java/io/grpc/examples/oauth/OAuth2ServerInterceptor.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2023 The gRPC 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
+ *
+ * http://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 io.grpc.examples.oauth;
+
+import io.grpc.Context;
+import io.grpc.Contexts;
+import io.grpc.Metadata;
+import io.grpc.ServerCall;
+import io.grpc.ServerCallHandler;
+import io.grpc.ServerInterceptor;
+import io.grpc.Status;
+
+/**
+ * This interceptor gets the OAuth2 access token from metadata, verifies it and sets the client
+ * identifier obtained from the token into the context. The one check it does on the access token
+ * is that the token has been refreshed at least once.
+ *
+ * A real implementation will validate the access token using the resource server (or the
+ * authorization server).
+ */
+class OAuth2ServerInterceptor implements ServerInterceptor {
+
+ private static final String BEARER_TYPE = "Bearer";
+
+ @Override
+ public ServerCall.Listener interceptCall(ServerCall serverCall,
+ Metadata metadata, ServerCallHandler serverCallHandler) {
+ String authHeaderValue = metadata.get(Constant.AUTHORIZATION_METADATA_KEY);
+
+ Status status = Status.OK;
+ if (authHeaderValue == null) {
+ status = Status.UNAUTHENTICATED.withDescription("Authorization token is missing");
+ } else if (!authHeaderValue.startsWith(BEARER_TYPE)) {
+ status = Status.UNAUTHENTICATED.withDescription("Unknown authorization type");
+ } else {
+ // remove authorization type prefix
+ String tokenValue = authHeaderValue.substring(BEARER_TYPE.length()).trim();
+ if (!tokenValue.startsWith(Constant.ACCESS_TOKEN)) {
+ status = Status.UNAUTHENTICATED.withDescription("Invalid access token authHeaderValue");
+ } else {
+ String[] tokens = tokenValue.split(":");
+ if (tokens.length >= 3 && tokens[2].equals(Constant.REFRESH_SUFFIX)) {
+ // set access tokenValue into current context
+ Context ctx = Context.current()
+ .withValue(Constant.CLIENT_ID_CONTEXT_KEY, tokens[1]);
+ return Contexts.interceptCall(ctx, serverCall, metadata, serverCallHandler);
+ } else {
+ status = Status.UNAUTHENTICATED.withDescription("stale credentials");
+ }
+ }
+ }
+
+ // at this point we have auth failure: skip further processing and close the call
+ serverCall.close(status, new Metadata());
+ return new ServerCall.Listener() {
+ // noop
+ };
+ }
+
+}
diff --git a/examples/example-oauth/src/main/proto/helloworld.proto b/examples/example-oauth/src/main/proto/helloworld.proto
new file mode 100644
index 00000000000..6340c54f7bb
--- /dev/null
+++ b/examples/example-oauth/src/main/proto/helloworld.proto
@@ -0,0 +1,37 @@
+// Copyright 2019 The gRPC 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
+//
+// http://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.
+syntax = "proto3";
+
+option java_multiple_files = true;
+option java_package = "io.grpc.examples.helloworld";
+option java_outer_classname = "HelloWorldProto";
+option objc_class_prefix = "HLW";
+
+package helloworld;
+
+// The greeting service definition.
+service Greeter {
+ // Sends a greeting
+ rpc SayHello (HelloRequest) returns (HelloReply) {}
+}
+
+// The request message containing the user's name.
+message HelloRequest {
+ string name = 1;
+}
+
+// The response message containing the greetings
+message HelloReply {
+ string message = 1;
+}
diff --git a/examples/example-oauth/src/test/java/io/grpc/examples/oauth/AuthClientTest.java b/examples/example-oauth/src/test/java/io/grpc/examples/oauth/AuthClientTest.java
new file mode 100644
index 00000000000..739018cc5a8
--- /dev/null
+++ b/examples/example-oauth/src/test/java/io/grpc/examples/oauth/AuthClientTest.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright 2023 The gRPC 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
+ *
+ * http://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 io.grpc.examples.oauth;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.AdditionalAnswers.delegatesTo;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
+import io.grpc.CallCredentials;
+import io.grpc.ManagedChannel;
+import io.grpc.Metadata;
+import io.grpc.ServerCall;
+import io.grpc.ServerCallHandler;
+import io.grpc.ServerInterceptors;
+import io.grpc.auth.MoreCallCredentials;
+import io.grpc.examples.helloworld.GreeterGrpc;
+import io.grpc.examples.helloworld.HelloReply;
+import io.grpc.examples.helloworld.HelloRequest;
+import io.grpc.inprocess.InProcessChannelBuilder;
+import io.grpc.inprocess.InProcessServerBuilder;
+import io.grpc.ServerCall.Listener;
+import io.grpc.ServerInterceptor;
+import io.grpc.stub.StreamObserver;
+import io.grpc.testing.GrpcCleanupRule;
+import java.io.IOException;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.ArgumentCaptor;
+import org.mockito.ArgumentMatchers;
+
+/**
+ * Unit tests for {@link AuthClient} testing the default and non-default tokens
+ *
+ *
+ */
+@RunWith(JUnit4.class)
+public class AuthClientTest {
+ /**
+ * This rule manages automatic graceful shutdown for the registered servers and channels at the
+ * end of test.
+ */
+ @Rule
+ public final GrpcCleanupRule grpcCleanup = new GrpcCleanupRule();
+
+ private final ServerInterceptor mockServerInterceptor = mock(ServerInterceptor.class, delegatesTo(
+ new ServerInterceptor() {
+ @Override
+ public Listener interceptCall(
+ ServerCall call, Metadata headers, ServerCallHandler next) {
+ return next.startCall(call, headers);
+ }
+ }));
+
+ private AuthClient client;
+
+
+ @Before
+ public void setUp() throws IOException {
+ // Generate a unique in-process server name.
+ String serverName = InProcessServerBuilder.generateName();
+
+ // Create a server, add service, start, and register for automatic graceful shutdown.
+ grpcCleanup.register(InProcessServerBuilder.forName(serverName).directExecutor()
+ .addService(ServerInterceptors.intercept(
+ new GreeterGrpc.GreeterImplBase() {
+
+ @Override
+ public void sayHello(
+ HelloRequest request, StreamObserver responseObserver) {
+ HelloReply reply = HelloReply.newBuilder()
+ .setMessage("AuthClientTest user=" + request.getName()).build();
+ responseObserver.onNext(reply);
+ responseObserver.onCompleted();
+ }
+ },
+ mockServerInterceptor))
+ .build().start());
+
+ CallCredentials credentials = MoreCallCredentials.from(
+ new ExampleOAuth2Credentials("test-client"));
+ ManagedChannel channel = InProcessChannelBuilder.forName(serverName).directExecutor().build();
+ client = new AuthClient(credentials, channel);
+ }
+
+ @Test
+ public void greet() {
+ ArgumentCaptor metadataCaptor = ArgumentCaptor.forClass(Metadata.class);
+ String retVal = client.greet("John");
+
+ verify(mockServerInterceptor).interceptCall(
+ ArgumentMatchers.>any(),
+ metadataCaptor.capture(),
+ ArgumentMatchers.>any());
+
+ String token = metadataCaptor.getValue().get(Constant.AUTHORIZATION_METADATA_KEY);
+ assertNotNull(token);
+ assertTrue(token.startsWith("Bearer"));
+ assertEquals("AuthClientTest user=John", retVal);
+ }
+}