[Oauth2-Middleware] Changes to introduce HOCON+pureconfig for oauth2-middleware (#12061)

* Changes to introduce HOCON+pureconfig for oauth2-middleware

CHANGELOG_BEGIN
CHANGELOG_END

* remove Cli arg parsing code + cleanup based on code review

* addition of a minimal config and changes to README.md

* keep existing cli args, but load from config file if provided

* fix broken docs build

* make tests OS independent

* Fail/error on supplying both config file and cli opts for startup, address code review comments
This commit is contained in:
akshayshirahatti-da 2021-12-16 16:36:32 +00:00 committed by GitHub
parent e52469c1dc
commit 4745768ad4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 592 additions and 147 deletions

View File

@ -15,10 +15,10 @@ OAuth 2.0 Configuration
`RFC 6749 <https://tools.ietf.org/html/rfc6749#section-3>`_ specifies that OAuth 2.0 providers offer two endpoints:
The `authorization endpoint <https://tools.ietf.org/html/rfc6749#section-3.1>`_
and the `token endpoint <https://tools.ietf.org/html/rfc6749#section-3.2>`_.
The URIs for these endpoints can be configured independently using the following flags:
The URIs for these endpoints can be configured independently using the following fields:
- ``--oauth-auth``
- ``--oauth-token``
- ``oauth-auth``
- ``oauth-token``
The OAuth 2.0 provider may require that the application identify itself using a client identifier and client secret.
These can be specified using the following environment variables:
@ -42,9 +42,9 @@ Authorization Request
=====================
This template defines the format of the `Authorization request <https://tools.ietf.org/html/rfc6749#section-4.1.1>`_.
Use the following command-line flag to use a custom template:
Use the following config field to use a custom template:
- ``--oauth-auth-template``
- ``oauth-auth-template``
Arguments
^^^^^^^^^
@ -93,9 +93,9 @@ Token Request
=============
This template defines the format of the `Token request <https://tools.ietf.org/html/rfc6749#section-4.1.3>`_.
Use the following command-line flag to use a custom template:
Use the following config field to use a custom template:
- ``--oauth-token-template``
- ``oauth-token-template``
Arguments
^^^^^^^^^
@ -131,9 +131,9 @@ Refresh Request
===============
This template defines the format of the `Refresh request <https://tools.ietf.org/html/rfc6749#section-6>`_.
Use the following command-line flag to use a custom template:
Use the following config field to use a custom template:
- ``--oauth-refresh-template``
- ``oauth-refresh-template``
Arguments
^^^^^^^^^
@ -187,12 +187,76 @@ For example, assuming the following nginx configuration snippet:
You would invoke the OAuth 2.0 auth middleware with the following flags:
.. code-block:: shell
oauth2-middleware \
--config oauth-middleware.conf
The required config would look like
.. code-block:: none
{
// Environment variables:
// DAML_CLIENT_ID The OAuth2 client-id - must not be empty
// DAML_CLIENT_SECRET The OAuth2 client-secret - must not be empty
client-id = ${DAML_CLIENT_ID}
client-secret = ${DAML_CLIENT_SECRET}
//IP address that OAuth2 Middleware service listens on. Defaults to 127.0.0.1.
address = "127.0.0.1"
//OAuth2 Middleware service port number. Defaults to 3000. A port number of 0 will let the system pick an ephemeral port. Consider specifying `--port-file` option with port number 0.
port = 3000
//URI to the auth middleware's callback endpoint `/cb`. By default constructed from the incoming login request.
callback-uri = "https://example.com/auth/cb"
//Maximum number of simultaneously pending login requests. Requests will be denied when exceeded until earlier requests have been completed or timed out.
max-login-requests = 250
//Login request timeout. Requests will be evicted if the callback endpoint receives no corresponding request in time.
login-timeout = 60s
//Enable the Secure attribute on the cookie that stores the token. Defaults to true. Only disable this for testing and development purposes.
cookie-secure = "true"
//URI of the OAuth2 authorization endpoint
oauth-auth="https://oauth2-provider.com/auth_uri"
//URI of the OAuth2 token endpoint
oauth-token="https://oauth2-provider.com/token_uri"
//OAuth2 authorization request Jsonnet template
oauth-auth-template="file://path/oauth/auth/template"
//OAuth2 token request Jsonnet template
oauth-token-template = "file://path/oauth/token/template"
//OAuth2 refresh request Jsonnet template
oauth-refresh-template = "file://path/oauth/refresh/template"
// Enables JWT-based authorization, where the JWT is signed by one of the below Jwt based token verifiers
token-verifier {
// type can be rs256-crt, es256-crt, es512-crt or rs256-jwks
type = "rs256-jwks"
// X509 certificate file (.crt)/JWKS url from where the public key is loaded
uri = "https://example.com/.well-known/jwks.json"
}
}
The oauth2-middleware can also be started using cli-args.
.. note:: Configuration file is the recommended way to run oauth2-middleware, running via cli-args is now deprecated
.. code-block:: shell
oauth2-middleware \
--callback https://example.com/auth/cb \
--address localhost
--http-port 3000
--address localhost \
--http-port 3000 \
--oauth-auth https://oauth2-provider.com/auth_uri \
--oauth-token https://oauth2-provider.com/token_uri \
--auth-jwt-rs256-jwks https://example.com/.well-known/jwks.json
Some browsers reject ``Secure`` cookies on unencrypted connections even on localhost.
You can pass the command-line flag ``--cookie-secure no`` for testing and development on localhost to avoid this.
You can pass the command-line flag ``--cookie-secure no`` for testing and development on localhost to avoid this.

View File

@ -71,6 +71,9 @@ da_scala_library(
"@maven//:com_typesafe_akka_akka_parsing",
"@maven//:com_typesafe_akka_akka_stream",
"@maven//:com_typesafe_scala_logging_scala_logging",
"@maven//:com_chuusai_shapeless",
"@maven//:com_github_pureconfig_pureconfig_core",
"@maven//:com_github_pureconfig_pureconfig_generic",
"@maven//:io_spray_spray_json",
"@maven//:org_scala_lang_modules_scala_collection_compat",
"@maven//:org_scalaz_scalaz_core",
@ -87,6 +90,8 @@ da_scala_library(
"//ledger/cli-opts",
"//ledger/ledger-api-auth",
"//libs-scala/ports",
"@maven//:com_auth0_java_jwt",
"@maven//:com_typesafe_config",
"@maven//:org_slf4j_slf4j_api",
],
)
@ -173,6 +178,10 @@ da_scala_test(
da_scala_test(
name = "oauth2-middleware-tests",
srcs = glob(["src/test/scala/com/daml/auth/middleware/oauth2/**/*.scala"]),
data = [
":src/test/resources/oauth2-middleware.conf",
":src/test/resources/oauth2-middleware-minimal.conf",
],
scala_deps = [
"@maven//:com_typesafe_akka_akka_actor",
"@maven//:com_typesafe_akka_akka_http",
@ -194,6 +203,7 @@ da_scala_test(
":oauth2-api",
":oauth2-middleware",
":oauth2-test-server",
"//bazel_tools/runfiles:scala_runfiles",
"//daml-lf/data",
"//language-support/scala/bindings",
"//ledger-api/rs-grpc-bridge",

View File

@ -0,0 +1,280 @@
// Copyright (c) 2021 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.auth.middleware.oauth2
import akka.http.scaladsl.model.Uri
import com.auth0.jwt.algorithms.Algorithm
import com.daml.auth.middleware.oauth2.Config.{
DefaultCookieSecure,
DefaultHttpPort,
DefaultLoginTimeout,
DefaultMaxLoginRequests,
}
import com.daml.cliopts
import com.daml.jwt.{
ECDSAVerifier,
HMAC256Verifier,
JwksVerifier,
JwtVerifierBase,
JwtVerifierConfigurationCli,
RSA256Verifier,
}
import com.typesafe.scalalogging.StrictLogging
import pureconfig.{ConfigReader, ConfigSource, ConvertHelpers}
import pureconfig.error.ConfigReaderException
import scopt.OptionParser
import java.io.File
import pureconfig.generic.semiauto._
import java.nio.file.{Path, Paths}
import scala.concurrent.duration
import scala.concurrent.duration.FiniteDuration
sealed trait ConfigError extends Product with Serializable {
def msg: String
}
case object MissingConfigError extends ConfigError {
val msg = "Missing auth middleware config file"
}
final case class ConfigParseError(msg: String) extends ConfigError
final case class Cli(
configFile: Option[File] = None,
// Host and port the middleware listens on
address: String = cliopts.Http.defaultAddress,
port: Int = DefaultHttpPort,
portFile: Option[Path] = None,
// The URI to which the OAuth2 server will redirect after a completed login flow.
// Must map to the `/cb` endpoint of the auth middleware.
callbackUri: Option[Uri] = None,
maxLoginRequests: Int = DefaultMaxLoginRequests,
loginTimeout: FiniteDuration = DefaultLoginTimeout,
cookieSecure: Boolean = DefaultCookieSecure,
// OAuth2 server endpoints
oauthAuth: Uri,
oauthToken: Uri,
// OAuth2 server request templates
oauthAuthTemplate: Option[Path],
oauthTokenTemplate: Option[Path],
oauthRefreshTemplate: Option[Path],
// OAuth2 client properties
clientId: String,
clientSecret: SecretString,
// Token verification
tokenVerifier: JwtVerifierBase,
) {
import Cli._
def loadConfigFromFile: Either[ConfigError, Config] = {
require(configFile.nonEmpty, "Config file should be defined to load app config")
configFile
.map(f =>
try {
Right(ConfigSource.file(f).loadOrThrow[Config])
} catch {
case ex: ConfigReaderException[_] => Left(ConfigParseError(ex.failures.head.description))
}
)
.get
}
def loadConfigFromCliArgs: Config = {
val cfg = Config(
address,
port,
portFile,
callbackUri,
maxLoginRequests,
loginTimeout,
cookieSecure,
oauthAuth,
oauthToken,
oauthAuthTemplate,
oauthTokenTemplate,
oauthRefreshTemplate,
clientId,
clientSecret,
tokenVerifier,
)
cfg.validate
cfg
}
}
object Cli extends StrictLogging {
implicit val tokenVerifierReader: ConfigReader[JwtVerifierBase] =
ConfigReader.forProduct2[JwtVerifierBase, String, String]("type", "uri") {
case (t: String, p: String) =>
// hs256-unsafe, rs256-crt, es256-crt, es512-crt, rs256-jwks
t match {
case "hs256-unsafe" =>
HMAC256Verifier(p)
.valueOr(err => sys.error(s"Failed to create HMAC256 verifier: $err"))
case "rs256-crt" =>
RSA256Verifier
.fromCrtFile(p)
.valueOr(err => sys.error(s"Failed to create RSA256 verifier: $err"))
case "es256-crt" =>
ECDSAVerifier
.fromCrtFile(p, Algorithm.ECDSA256(_, null))
.valueOr(err => sys.error(s"Failed to create ECDSA256 verifier: $err"))
case "es512-crt" =>
ECDSAVerifier
.fromCrtFile(p, Algorithm.ECDSA512(_, null))
.valueOr(err => sys.error(s"Failed to create ECDSA512 verifier: $err"))
case "rs256-jwks" =>
JwksVerifier(p)
}
}
lazy implicit val uriReader: ConfigReader[Uri] =
ConfigReader.fromString[Uri](ConvertHelpers.catchReadError(s => Uri(s)))
lazy implicit val clientSecretReader: ConfigReader[SecretString] =
ConfigReader.fromString[SecretString](ConvertHelpers.catchReadError(s => SecretString(s)))
lazy implicit val cfgReader: ConfigReader[Config] = deriveReader[Config]
private val Empty =
Cli(
configFile = None,
address = cliopts.Http.defaultAddress,
port = DefaultHttpPort,
portFile = None,
callbackUri = None,
maxLoginRequests = DefaultMaxLoginRequests,
loginTimeout = DefaultLoginTimeout,
cookieSecure = DefaultCookieSecure,
oauthAuth = null,
oauthToken = null,
oauthAuthTemplate = None,
oauthTokenTemplate = None,
oauthRefreshTemplate = None,
clientId = null,
clientSecret = null,
tokenVerifier = null,
)
private val parser: OptionParser[Cli] = new scopt.OptionParser[Cli]("oauth-middleware") {
help('h', "help").text("Print usage")
opt[Option[File]]('c', "config")
.text(
"This is the recommended way to provide an app config file, the remaining cli-args are deprecated"
)
.valueName("<file>")
.action((file, cli) => cli.copy(configFile = file))
cliopts.Http.serverParse(this, serviceName = "OAuth2 Middleware")(
address = (f, c) => c.copy(address = f(c.address)),
httpPort = (f, c) => c.copy(port = f(c.port)),
defaultHttpPort = Some(DefaultHttpPort),
portFile = Some((f, c) => c.copy(portFile = f(c.portFile))),
)
opt[String]("callback")
.action((x, c) => c.copy(callbackUri = Some(Uri(x))))
.text(
"URI to the auth middleware's callback endpoint `/cb`. By default constructed from the incoming login request."
)
opt[Int]("max-pending-login-requests")
.action((x, c) => c.copy(maxLoginRequests = x))
.text(
"Maximum number of simultaneously pending login requests. Requests will be denied when exceeded until earlier requests have been completed or timed out."
)
opt[Boolean]("cookie-secure")
.action((x, c) => c.copy(cookieSecure = x))
.text(
"Enable the Secure attribute on the cookie that stores the token. Defaults to true. Only disable this for testing and development purposes."
)
opt[Long]("login-request-timeout")
.action((x, c) => c.copy(loginTimeout = FiniteDuration(x, duration.SECONDS)))
.text(
"Login request timeout. Requests will be evicted if the callback endpoint receives no corresponding request in time."
)
opt[String]("oauth-auth")
.action((x, c) => c.copy(oauthAuth = Uri(x)))
.text("URI of the OAuth2 authorization endpoint")
opt[String]("oauth-token")
.action((x, c) => c.copy(oauthToken = Uri(x)))
.text("URI of the OAuth2 token endpoint")
opt[String]("oauth-auth-template")
.action((x, c) => c.copy(oauthAuthTemplate = Some(Paths.get(x))))
.text("OAuth2 authorization request Jsonnet template")
opt[String]("oauth-token-template")
.action((x, c) => c.copy(oauthTokenTemplate = Some(Paths.get(x))))
.text("OAuth2 token request Jsonnet template")
opt[String]("oauth-refresh-template")
.action((x, c) => c.copy(oauthRefreshTemplate = Some(Paths.get(x))))
.text("OAuth2 refresh request Jsonnet template")
opt[String]("id")
.hidden()
.action((x, c) => c.copy(clientId = x))
.withFallback(() => sys.env.getOrElse("DAML_CLIENT_ID", ""))
opt[String]("secret")
.hidden()
.action((x, c) => c.copy(clientSecret = SecretString(x)))
.withFallback(() => sys.env.getOrElse("DAML_CLIENT_SECRET", ""))
JwtVerifierConfigurationCli.parse(this)((v, c) => c.copy(tokenVerifier = v))
checkConfig { cfg =>
if (cfg.configFile.isEmpty && cfg.tokenVerifier == null)
Left("You must specify one of the --auth-jwt-* flags for token verification.")
else
Right(())
}
checkConfig { cfg =>
if (cfg.configFile.isEmpty && (cfg.clientId.isEmpty || cfg.clientSecret.value.isEmpty))
Left("Environment variable DAML_CLIENT_ID AND DAML_CLIENT_SECRET must not be empty")
else
Right(())
}
checkConfig { cfg =>
if (cfg.configFile.isEmpty && (cfg.oauthAuth == null || cfg.oauthToken == null))
Left("oauth-auth and oauth-token values must not be empty")
else
Right(())
}
checkConfig { cfg =>
val cliOptionsAreDefined =
cfg.oauthToken != null || cfg.oauthAuth != null || cfg.tokenVerifier != null
if (cfg.configFile.isDefined && cliOptionsAreDefined) {
Left("Found both config file and cli opts for the app, please provide only one of them")
} else Right(())
}
override def showUsageOnError: Option[Boolean] = Some(true)
}
def parse(args: Array[String]): Option[Cli] = parser.parse(args, Empty)
def parseConfig(args: Array[String]): Option[Config] = {
val cli = parse(args)
cli.flatMap { c =>
if (c.configFile.isDefined) {
c.loadConfigFromFile match {
case Right(conf) => Some(conf)
case Left(err) =>
logger.error(s"Unable to start oauth2-middleware using config: ${err.msg}")
None
}
} else {
logger.warn("Using cli opts for running oauth2-middleware is deprecated")
Some(c.loadConfigFromCliArgs)
}
}
}
}

View File

@ -3,26 +3,26 @@
package com.daml.auth.middleware.oauth2
import java.nio.file.{Path, Paths}
import java.nio.file.Path
import akka.http.scaladsl.model.Uri
import com.daml.auth.middleware.oauth2.Config._
import com.daml.cliopts
import com.daml.jwt.{JwtVerifierBase, JwtVerifierConfigurationCli}
import com.daml.jwt.JwtVerifierBase
import scala.concurrent.duration
import scala.concurrent.duration.FiniteDuration
case class Config(
// Host and port the middleware listens on
address: String,
port: Int,
portFile: Option[Path],
address: String = cliopts.Http.defaultAddress,
port: Int = DefaultHttpPort,
portFile: Option[Path] = None,
// The URI to which the OAuth2 server will redirect after a completed login flow.
// Must map to the `/cb` endpoint of the auth middleware.
callbackUri: Option[Uri],
maxLoginRequests: Int,
loginTimeout: FiniteDuration,
cookieSecure: Boolean,
callbackUri: Option[Uri] = None,
maxLoginRequests: Int = DefaultMaxLoginRequests,
loginTimeout: FiniteDuration = DefaultLoginTimeout,
cookieSecure: Boolean = DefaultCookieSecure,
// OAuth2 server endpoints
oauthAuth: Uri,
oauthToken: Uri,
@ -35,7 +35,15 @@ case class Config(
clientSecret: SecretString,
// Token verification
tokenVerifier: JwtVerifierBase,
)
) {
def validate: Unit = {
require(oauthToken != null, "Oauth token value on config cannot be null")
require(oauthAuth != null, "Oauth auth value on config cannot be null")
require(clientId.nonEmpty, "DAML_CLIENT_ID cannot be empty")
require(clientSecret.value.nonEmpty, "DAML_CLIENT_SECRET cannot be empty")
require(tokenVerifier != null, "token verifier must be defined")
}
}
case class SecretString(value: String) {
override def toString: String = "###"
@ -46,120 +54,4 @@ object Config {
val DefaultCookieSecure: Boolean = true
val DefaultMaxLoginRequests: Int = 100
val DefaultLoginTimeout: FiniteDuration = FiniteDuration(5, duration.MINUTES)
private val Empty =
Config(
address = cliopts.Http.defaultAddress,
port = DefaultHttpPort,
portFile = None,
callbackUri = None,
maxLoginRequests = DefaultMaxLoginRequests,
loginTimeout = DefaultLoginTimeout,
cookieSecure = DefaultCookieSecure,
oauthAuth = null,
oauthToken = null,
oauthAuthTemplate = None,
oauthTokenTemplate = None,
oauthRefreshTemplate = None,
clientId = null,
clientSecret = null,
tokenVerifier = null,
)
def parseConfig(args: collection.Seq[String]): Option[Config] =
configParser.parse(args, Empty)
@SuppressWarnings(Array("org.wartremover.warts.NonUnitStatements"))
val configParser: scopt.OptionParser[Config] =
new scopt.OptionParser[Config]("oauth-middleware") {
head("OAuth2 Middleware")
cliopts.Http.serverParse(this, serviceName = "OAuth2 Middleware")(
address = (f, c) => c.copy(address = f(c.address)),
httpPort = (f, c) => c.copy(port = f(c.port)),
defaultHttpPort = Some(DefaultHttpPort),
portFile = Some((f, c) => c.copy(portFile = f(c.portFile))),
)
opt[String]("callback")
.action((x, c) => c.copy(callbackUri = Some(Uri(x))))
.text(
"URI to the auth middleware's callback endpoint `/cb`. By default constructed from the incoming login request."
)
opt[Int]("max-pending-login-requests")
.action((x, c) => c.copy(maxLoginRequests = x))
.text(
"Maximum number of simultaneously pending login requests. Requests will be denied when exceeded until earlier requests have been completed or timed out."
)
opt[Boolean]("cookie-secure")
.action((x, c) => c.copy(cookieSecure = x))
.text(
"Enable the Secure attribute on the cookie that stores the token. Defaults to true. Only disable this for testing and development purposes."
)
opt[Long]("login-request-timeout")
.action((x, c) => c.copy(loginTimeout = FiniteDuration(x, duration.SECONDS)))
.text(
"Login request timeout. Requests will be evicted if the callback endpoint receives no corresponding request in time."
)
opt[String]("oauth-auth")
.action((x, c) => c.copy(oauthAuth = Uri(x)))
.required()
.text("URI of the OAuth2 authorization endpoint")
opt[String]("oauth-token")
.action((x, c) => c.copy(oauthToken = Uri(x)))
.required()
.text("URI of the OAuth2 token endpoint")
opt[String]("oauth-auth-template")
.action((x, c) => c.copy(oauthAuthTemplate = Some(Paths.get(x))))
.text("OAuth2 authorization request Jsonnet template")
opt[String]("oauth-token-template")
.action((x, c) => c.copy(oauthTokenTemplate = Some(Paths.get(x))))
.text("OAuth2 token request Jsonnet template")
opt[String]("oauth-refresh-template")
.action((x, c) => c.copy(oauthRefreshTemplate = Some(Paths.get(x))))
.text("OAuth2 refresh request Jsonnet template")
opt[String]("id")
.hidden()
.action((x, c) => c.copy(clientId = x))
.withFallback(() => sys.env.getOrElse("DAML_CLIENT_ID", ""))
.validate(x =>
if (x.isEmpty) failure("Environment variable DAML_CLIENT_ID must not be empty")
else success
)
opt[String]("secret")
.hidden()
.action((x, c) => c.copy(clientSecret = SecretString(x)))
.withFallback(() => sys.env.getOrElse("DAML_CLIENT_SECRET", ""))
.validate(x =>
if (x.isEmpty) failure("Environment variable DAML_CLIENT_SECRET must not be empty")
else success
)
JwtVerifierConfigurationCli.parse(this)((v, c) => c.copy(tokenVerifier = v))
checkConfig { cfg =>
if (cfg.tokenVerifier == null)
Left("You must specify one of the --auth-jwt-* flags for token verification.")
else
Right(())
}
help("help").text("Print this usage text")
note("""
|Environment variables:
| DAML_CLIENT_ID The OAuth2 client-id - must not be empty
| DAML_CLIENT_SECRET The OAuth2 client-secret - must not be empty
""".stripMargin)
}
}

View File

@ -11,7 +11,7 @@ import scala.util.{Failure, Success}
object Main extends StrictLogging {
def main(args: Array[String]): Unit = {
Config.parseConfig(args) match {
Cli.parseConfig(args) match {
case Some(config) => main(config)
case None => sys.exit(1)
}

View File

@ -79,17 +79,30 @@ repository](https://github.com/digital-asset/ex-secure-daml-infra).
$ DAML_CLIENT_ID=CLIENTID \
DAML_CLIENT_SECRET=CLIENTSECRET \
bazel run //triggers/service/auth:oauth-middleware-binary -- \
--port 3000 \
--oauth-auth AUTHURL \
--oauth-token TOKENURL
--config oauth-middleware.conf
```
- Replace `CLIENTID` and `CLIENTSECRET` by the "Client ID" and "Client
Secret" from above.
- Replace `AUTHURL` and `TOKENURL` by the "OAuth Authorization URL"
and "OAuth Token URL" from above. They should look as follows:
The basic minimal config that needs to be supplied needs to have appropriate
`callback-uri`,`oauth-auth` and `oauth-token` urls defined,
along with the `token-verifier`,`client-id` and `client-secret` fields. e.g
```
https://XYZ.auth0.com/authorize
https://XYZ.auth0.com/oauth/token
{
callback-uri = "https://example.com/auth/cb"
oauth-auth = "https://XYZ.auth0.com/authorize"
oauth-token = "https://XYZ.auth0.com/oauth/token"
client-id = ${DAML_CLIENT_ID}
client-secret = ${DAML_CLIENT_SECRET}
// type can be one of rs256-crt, es256-crt, es512-crt, rs256-jwks
// uri is the uri to the cert file or the jwks url
token-verifier {
type = "rs256-jwks"
uri = "https://example.com/.well-known/jwks.json"
}
}
```
- Browse to the middleware's login endpoint.
- URL `http://localhost:3000/login?redirect_uri=callback&claims=actAs:Alice`

View File

@ -0,0 +1,18 @@
{
callback-uri = "https://example.com/auth/cb"
oauth-auth = "https://oauth2/uri"
oauth-token = "https://oauth2/token"
// client-id = ${DAML_CLIENT_ID}
// client-secret = ${DAML_CLIENT_SECRET}
// can be set via env variables , dummy values for test purposes
client-id = foo
client-secret = bar
// type can be one of rs256-crt, es256-crt, es512-crt, rs256-jwks
// uri is the uri to the cert file or the jwks url
token-verifier {
type = "rs256-jwks"
uri = "https://example.com/.well-known/jwks.json"
}
}

View File

@ -0,0 +1,27 @@
{
address = "127.0.0.1"
port = 3000
callback-uri = "https://example.com/auth/cb"
max-login-requests = 10
login-timeout = 60s
cookie-secure = false
oauth-auth = "https://oauth2/uri"
oauth-token = "https://oauth2/token"
oauth-auth-template = "auth_template"
oauth-token-template = "token_template"
oauth-refresh-template = "refresh_template"
// client-id = ${DAML_CLIENT_ID}
// client-secret = ${DAML_CLIENT_SECRET}
// can be set via env variables , dummy values for test purposes
client-id = foo
client-secret = bar
// type can be one of rs256-crt, es256-crt, es512-crt, rs256-jwks
// uri is the uri to the cert file or the jwks url
token-verifier {
type = "rs256-jwks"
uri = "https://example.com/.well-known/jwks.json"
}
}

View File

@ -0,0 +1,141 @@
// Copyright (c) 2021 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.auth.middleware.oauth2
import akka.http.scaladsl.model.Uri
import org.scalatest.matchers.should.Matchers
import org.scalatest.wordspec.AsyncWordSpec
import com.daml.bazeltools.BazelRunfiles.requiredResource
import com.daml.jwt.JwksVerifier
import java.nio.file.Paths
import scala.concurrent.duration._
class CliSpec extends AsyncWordSpec with Matchers {
val confFile = "triggers/service/auth/src/test/resources/oauth2-middleware.conf"
def loadCli(file: String): Cli = {
Cli.parse(Array("--config", file)).getOrElse(fail("Could not load Cli on parse"))
}
"should pickup the config file provided" in {
val file = requiredResource(confFile)
val cli = loadCli(file.getAbsolutePath)
cli.configFile should not be empty
}
"should take default values on loading minimal config" in {
val file =
requiredResource("triggers/service/auth/src/test/resources/oauth2-middleware-minimal.conf")
val cli = loadCli(file.getAbsolutePath)
cli.configFile should not be empty
cli.loadConfigFromFile match {
case Left(ex) => fail(ex.msg)
case Right(c) =>
c.address shouldBe "127.0.0.1"
c.port shouldBe Config.DefaultHttpPort
c.callbackUri shouldBe Some(Uri("https://example.com/auth/cb"))
c.maxLoginRequests shouldBe Config.DefaultMaxLoginRequests
c.loginTimeout shouldBe Config.DefaultLoginTimeout
c.cookieSecure shouldBe Config.DefaultCookieSecure
c.oauthAuth shouldBe Uri("https://oauth2/uri")
c.oauthToken shouldBe Uri("https://oauth2/token")
c.oauthAuthTemplate shouldBe None
c.oauthTokenTemplate shouldBe None
c.oauthRefreshTemplate shouldBe None
c.clientId shouldBe sys.env.getOrElse("DAML_CLIENT_ID", "foo")
c.clientSecret shouldBe SecretString(sys.env.getOrElse("DAML_CLIENT_SECRET", "bar"))
// token verifier needs to be set.
c.tokenVerifier match {
case _: JwksVerifier => succeed
case _ => fail("expected JwksVerifier based on supplied config")
}
}
}
"should be able to successfully load the config based on the file provided" in {
val file = requiredResource(confFile)
val cli = loadCli(file.getAbsolutePath)
cli.configFile should not be empty
cli.loadConfigFromFile match {
case Left(ex) => fail(ex.msg)
case Right(c) =>
c.address shouldBe "127.0.0.1"
c.port shouldBe 3000
c.callbackUri shouldBe Some(Uri("https://example.com/auth/cb"))
c.maxLoginRequests shouldBe 10
c.loginTimeout shouldBe FiniteDuration(60, SECONDS)
c.cookieSecure shouldBe false
c.oauthAuth shouldBe Uri("https://oauth2/uri")
c.oauthToken shouldBe Uri("https://oauth2/token")
c.oauthAuthTemplate shouldBe Some(Paths.get("auth_template"))
c.oauthTokenTemplate shouldBe Some(Paths.get("token_template"))
c.oauthRefreshTemplate shouldBe Some(Paths.get("refresh_template"))
c.clientId shouldBe sys.env.getOrElse("DAML_CLIENT_ID", "foo")
c.clientSecret shouldBe SecretString(sys.env.getOrElse("DAML_CLIENT_SECRET", "bar"))
c.tokenVerifier match {
case _: JwksVerifier => succeed
case _ => fail("expected JwksVerifier based on supplied config")
}
}
}
"parse should raise error on non-existent config file" in {
val cli = loadCli("missingFile.conf")
cli.configFile should not be empty
val cfg = cli.loadConfigFromFile
cfg match {
case Left(err) => err shouldBe a[ConfigParseError]
case _ => fail("Expected a `ConfigParseError` on missing conf file")
}
//parseConfig for non-existent file should return a None
Cli.parseConfig(
Array(
"--config-file",
"missingFile.conf",
)
) shouldBe None
}
"should load config from cli args on missing conf file " in {
Cli
.parseConfig(
Array(
"--oauth-auth",
"file://foo",
"--oauth-token",
"file://bar",
"--id",
"foo",
"--secret",
"bar",
"--auth-jwt-hs256-unsafe",
"unsafe",
)
) should not be empty
}
"should fail to load config from cli args on incomplete cli args" in {
Cli
.parseConfig(
Array(
"--oauth-auth",
"file://foo",
"--id",
"foo",
"--secret",
"bar",
"--auth-jwt-hs256-unsafe",
"unsafe",
)
) shouldBe None
}
}