diff --git a/build.gradle b/build.gradle index 9e4d492f631..1a6d8a6ae65 100644 --- a/build.gradle +++ b/build.gradle @@ -149,6 +149,10 @@ cargo { containerId = "tomcat9x" port = applicationPort + if (JavaVersion.current() < JavaVersion.VERSION_11) { + throw new GradleException("This build must be run with Java version [ " + JavaVersion.VERSION_11 + " ] or greater. Your Java version is [ " + JavaVersion.current() + " ]") + } + deployable { file = file("samples/api/build/libs/cloudfoundry-identity-api-" + version + ".war") context = "api" @@ -166,7 +170,7 @@ cargo { local { configHomeDir = file(Paths.get(System.getProperty("java.io.tmpdir") + "/uaa-${applicationPort}")) - startStopTimeout = 540000 + startStopTimeout = Integer.parseInt(System.getProperty("startStopTimeout", "540000")) rmiPort = applicationPort + 10 jvmArgs = "" @@ -176,6 +180,11 @@ cargo { if (System.getProperty("spring.profiles.active", "").split(',').contains("debug")) { jvmArgs = String.format("%s -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005", jvmArgs) } + else if (Boolean.valueOf(System.getProperty("xdebug"))) { + jvmArgs = String.format("%s -Xdebug " + + "-Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005 " + + "-Xnoagent -Djava.compiler=NONE", jvmArgs) + } outputFile = file("uaa/build/reports/tests/uaa-server.log") configFile { diff --git a/dependencies.gradle b/dependencies.gradle index 813a85da8cc..047577dc3ba 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -10,15 +10,16 @@ ext["flyway.version"] = "5.2.4" // Versions shared between multiple dependencies versions.aspectJVersion = "1.9.4" versions.apacheDsVersion = "2.0.0.AM26" -versions.bouncyCastleVersion = "1.68" +versions.bouncyCastleVersion = "1.69" versions.hamcrestVersion = "2.2" -versions.springBootVersion = "2.4.2" +versions.springBootVersion = "2.4.9" versions.springSecurityJwtVersion = "1.1.1.RELEASE" -versions.springSecurityOAuthVersion = "2.5.0.RELEASE" +versions.springSecurityOAuthVersion = "2.5.1.RELEASE" versions.springSecuritySamlVersion = "1.0.10.RELEASE" -versions.springVersion = "5.3.3" +versions.springVersion = "5.3.9" versions.xmlBind = "2.3.0.1" -versions.tomcatCargoVersion = "9.0.41" +versions.tomcatCargoVersion = "9.0.50" +versions.guavaVersion = "30.1.1-jre" // Dependencies (some rely on shared versions, some are shared between projects) libraries.apacheCommonsRngCore = "org.apache.commons:commons-rng-core:1.3" @@ -31,13 +32,14 @@ libraries.aspectJWeaver = "org.aspectj:aspectjweaver" libraries.beanutils = "commons-beanutils:commons-beanutils:1.9.4" libraries.bouncyCastlePkix = "org.bouncycastle:bcpkix-jdk15on:${versions.bouncyCastleVersion}" libraries.bouncyCastleProv = "org.bouncycastle:bcprov-jdk15on:${versions.bouncyCastleVersion}" -libraries.commonsIo = "commons-io:commons-io:2.7" +libraries.commonsIo = "commons-io:commons-io:2.11.0" libraries.dumbster = "dumbster:dumbster:1.6" libraries.eclipseJgit = "org.eclipse.jgit:org.eclipse.jgit:5.8.0.202006091008-r" libraries.flywayCore = "org.flywaydb:flyway-core" libraries.greenmail = "com.icegreen:greenmail:1.5.11" libraries.googleAuth = "com.warrenstrange:googleauth:1.5.0" -libraries.guava = "com.google.guava:guava:30.0-jre" +libraries.guava = "com.google.guava:guava:${versions.guavaVersion}" +libraries.guavaTestLib = "com.google.guava:guava-testlib:${versions.guavaVersion}" libraries.hamcrest = "org.hamcrest:hamcrest:${versions.hamcrestVersion}" libraries.hibernateValidator = "org.hibernate.validator:hibernate-validator" libraries.hsqldb = "org.hsqldb:hsqldb" @@ -63,7 +65,7 @@ libraries.lombok = "org.projectlombok:lombok" libraries.mariaJdbcDriver = "org.mariadb.jdbc:mariadb-java-client" libraries.mockito = "org.mockito:mockito-core" libraries.mockitoJunit5 = "org.mockito:mockito-junit-jupiter" -libraries.passay = "org.passay:passay:1.6.0" +libraries.passay = "org.passay:passay:1.6.1" libraries.postgresql = "org.postgresql:postgresql" libraries.selenium = "org.seleniumhq.selenium:selenium-java" libraries.slf4jApi = "org.slf4j:slf4j-api" @@ -110,6 +112,7 @@ libraries.unboundIdLdapSdk = "com.unboundid:unboundid-ldapsdk" libraries.unboundIdScimSdk = "com.unboundid.product.scim:scim-sdk:1.8.24" libraries.velocity = "org.apache.velocity:velocity-engine-core:2.2" libraries.zxing = "com.google.zxing:javase:3.4.0" +libraries.nimbusJwt = "com.nimbusds:nimbus-jose-jwt" // gradle plugins libraries.asciidoctorGradlePlugin = "org.asciidoctor:asciidoctor-gradle-plugin:1.6.1" diff --git a/docs/github-oauth2-provider.md b/docs/github-oauth2-provider.md new file mode 100644 index 00000000000..e3125d38590 --- /dev/null +++ b/docs/github-oauth2-provider.md @@ -0,0 +1,46 @@ +# Registering Github as external OAuth provider in UAA + +Github can be setup as an Oauth2 provider for UAA. + +1. Create an OAuth “application” client in Github. + For example at: `https://github.com/organizations/{YOUR-ORG}/settings/applications/new`. + + Add following URI in the “_Authorization callback URL_” text field: + `http://{UAA_HOST}/login/callback/{origin}`. Additional Github + documentation for achieving this can be found here: + [Creating an OAuth App](https://docs.github.com/en/free-pro-team@latest/developers/apps/creating-an-oauth-app) + [Authorizing OAuth Apps](https://docs.github.com/en/free-pro-team@latest/developers/apps/authorizing-oauth-apps) + +2. Make sure you have `Client ID` and `Client secret`. + +3. The following configuration needs to be added in login.yml. + Please refer to 'https://accounts.google.com/.well-known/openid-configuration' for authUrl and tokenUrl + + login: + oauth: + providers: + github: + type: oauth2.0 + providerDescription: Github OAuth provider, using the 'Authorization Code Grant' flow + authUrl: https://github.com/login/oauth/authorize + tokenUrl: https://github.com/login/oauth/access_token + userInfoUrl: https://api.github.com/user + scopes: + - read:user + - user:email + linkText: Login with Github + showLinkText: true + addShadowUserOnLogin: true # users won't need to be pre-populated into the UAA database prior to authenticating with Github + relyingPartyId: REPLACE_WITH_CLIENT_ID + relyingPartySecret: REPLACE_WITH_CLIENT_SECRET + skipSslValidation: false + clientAuthInBody: true + attributeMappings: + given_name: login + family_name: name # Github doesn't split 'given_name' and 'family_name' + user_name: email + +4. Ensure that the scope `email` is included in the`scopes` property. Without + this, UAA will not be able to identify the authenticated user. + +5. Restart UAA. You will see `Login with github` link on your login page. diff --git a/docs/okta-public-oidc-provider.md b/docs/okta-public-oidc-provider.md new file mode 100644 index 00000000000..c27753755cc --- /dev/null +++ b/docs/okta-public-oidc-provider.md @@ -0,0 +1,33 @@ +# Registering Okta as external, public OIDC provider in UAA + +Okta can be setup as an [OIDC provider](https://developer.okta.com/docs/guides/add-an-external-idp/openidconnect/configure-idp-in-okta/) for UAA login. +In order to prevent storing a client secret in UAA configuration and all of it's successor problems like secret rotation and so on, register the +external OIDC provider with a public client. + +1. Create an OIDC application and set it with [PKCE public](https://developer.okta.com/blog/2019/08/22/okta-authjs-pkce#use-pkce-to-make-your-apps-more-secure). + Register the "Redirect URIs" in the application section "OpenID Connect Configuration" + + Add following URI in list field: + `http://{UAA_HOST}/login/callback/{origin}`. [Additional documentation for achieving this can be found here](https://developer.okta.com/docs/guides/implement-auth-code-pkce/overview/). + +2. Copy client id. + +3. Minimal OIDC configuration needs to be added in login.ym. + Read configuration refer to 'https://.okta.com/.well-known/openid-configuration' for discoveryUrl and issuer + + login: + oauth: + providers: + okta.public: + type: oidc1.0 + discoveryUrl: https://trailaccount.okta.com/.well-known/openid-configuration + issuer: https://trailaccount.okta.com + scopes: + - openid + linkText: Login with Okta-Public + showLinkText: true + relyingPartyId: 0iak4aiaC4HV39L6g123 + +4. Ensure that the scope `openid` is included in the`scopes` property. + +5. Restart UAA. You will see `Login with Okta-Public` link on your login page. diff --git a/docs/sap-public-oidc-provider.md b/docs/sap-public-oidc-provider.md new file mode 100644 index 00000000000..2ff5850b23f --- /dev/null +++ b/docs/sap-public-oidc-provider.md @@ -0,0 +1,36 @@ +# Registering SAP IAS as external, public OIDC provider in UAA + +SAP IAS can be setup as an [OIDC provider](https://help.sap.com/viewer/6d6d63354d1242d185ab4830fc04feb1/Cloud/en-US/a789c9c8c0f5439da8c30b5d9e43bece.htm) for UAA login. +In order to prevent storing a client secret in UAA configuration and all of it's successor problems like secret rotation and so on, register the +external OIDC provider with a public client. + +1. Create an OIDC application and set it with [type public](https://help.sap.com/viewer/6d6d63354d1242d185ab4830fc04feb1/Cloud/en-US/a721157cd40544eb9bad40085cf8ec15.html). + Register the "Redirect URIs" in the application section "OpenID Connect Configuration" + + Add following URI in list field: + `http://{UAA_HOST}/login/callback/{origin}`. [Additional documentation for achieving this can be found here](https://help.sap.com/viewer/6d6d63354d1242d185ab4830fc04feb1/Cloud/en-US/1ae324ee3b2d4a728650eb022d5fd910.html). + +2. Copy client id. + +3. Minimal OIDC configuration needs to be added in login.ym. + Read configuration refer to '[https://.accounts.ondemand.com/.well-known/openid-configuration](https://help.sap.com/viewer/6d6d63354d1242d185ab4830fc04feb1/Cloud/en-US/c297516bae4547eb82eeed80fea2b937.html)' for discoveryUrl and issuer + + login: + oauth: + providers: + ias.public: + type: oidc1.0 + discoveryUrl: https://trailaccount.accounts.ondemand.com/.well-known/openid-configuration + issuer: https://trailaccount.accounts.ondemand.com + scopes: + - openid + - email + - profile + linkText: Login with IAS-Public + showLinkText: true + relyingPartyId: 3feb7ecb-d106-4432-b335-aca2689ad123 + +4. Ensure that the scope `openid`, `email` and `profile` is included in the`scopes` property. Then UAA shadow user (if addShadowUserOnLogin=true) is created + with all properties. + +5. Restart UAA. You will see `Login with IAS-Public` link on your login page. diff --git a/k8s/go.mod b/k8s/go.mod index 2544264bf88..734df681965 100644 --- a/k8s/go.mod +++ b/k8s/go.mod @@ -3,10 +3,10 @@ module github.com/cloudfoundry/uaa go 1.15 require ( - github.com/onsi/ginkgo v1.14.2 - github.com/onsi/gomega v1.10.4 + github.com/onsi/ginkgo v1.16.4 + github.com/onsi/gomega v1.14.0 gopkg.in/yaml.v2 v2.4.0 - k8s.io/api v0.20.2 - k8s.io/apimachinery v0.20.2 - k8s.io/client-go v0.20.2 + k8s.io/api v0.22.0 + k8s.io/apimachinery v0.22.0 + k8s.io/client-go v0.22.0 ) diff --git a/k8s/go.sum b/k8s/go.sum index 78d10abc292..27e5b1694d6 100644 --- a/k8s/go.sum +++ b/k8s/go.sum @@ -22,13 +22,11 @@ cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0Zeo cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= -github.com/Azure/go-autorest/autorest v0.11.1/go.mod h1:JFgpikqFJ/MleTTxwepExTKnFUKKszPS8UavbQYUMuw= -github.com/Azure/go-autorest/autorest/adal v0.9.0/go.mod h1:/c022QCutn2P7uY+/oQWWNcK9YU+MH96NgK+jErpbcg= -github.com/Azure/go-autorest/autorest/adal v0.9.5/go.mod h1:B7KF7jKIeC9Mct5spmyCB/A8CG/sEz1vwIRGv/bbw7A= +github.com/Azure/go-autorest/autorest v0.11.18/go.mod h1:dSiJPy22c3u0OtOKDNttNgqpNFY/GeWa7GH/Pz56QRA= +github.com/Azure/go-autorest/autorest/adal v0.9.13/go.mod h1:W/MM4U6nLxnIskrw4UwWzlHfGjwUS50aOsc/I3yuU8M= github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74= -github.com/Azure/go-autorest/autorest/mocks v0.4.0/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= -github.com/Azure/go-autorest/logger v0.2.0/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= +github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= @@ -41,42 +39,38 @@ github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWR github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= -github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/evanphx/json-patch v4.11.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= -github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= +github.com/form3tech-oss/jwt-go v3.2.3+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= -github.com/go-logr/logr v0.2.0 h1:QvGt2nLcHH0WK9orKa+ppBPAxREcH364nPUedEpK0TY= -github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= -github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg= +github.com/go-logr/logr v0.4.0 h1:K7/B1jt6fIBQVd4Owv2MqGQClcgf0R266+7C/QjRcLc= +github.com/go-logr/logr v0.4.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= -github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc= github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8= -github.com/go-openapi/spec v0.19.3/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo= -github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= -github.com/gogo/protobuf v1.3.1 h1:DqDEcV5aeaTmdFBePNpYsp3FlcVH/2ISVVM9Qf8PSls= -github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= @@ -84,7 +78,6 @@ github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= @@ -94,22 +87,19 @@ github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrU github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= -github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM= -github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM= -github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw= +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g= github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -124,32 +114,32 @@ github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+ github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3ir6b65WBswg= +github.com/googleapis/gnostic v0.5.1/go.mod h1:6U4PtQXGIEt/Z3h5MAT7FNofLnw9vXk2cUuW7uA/OeU= +github.com/googleapis/gnostic v0.5.5/go.mod h1:7+EbHbldMins07ALC74bsA81Ovc97DwqyJO1AENw9kA= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= -github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68= -github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.11 h1:uVUAXhF2To8cbw/3xN3pxj6kk7TYKs98NIrTqPlMWAQ= +github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= -github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA= -github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -158,21 +148,22 @@ github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9 github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= -github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= +github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= -github.com/onsi/ginkgo v1.14.2 h1:8mVmC9kjFFmA8H4pKMUhcblgifdkOIXPvbhN1T36q1M= -github.com/onsi/ginkgo v1.14.2/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= +github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= +github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= +github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= -github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= -github.com/onsi/gomega v1.10.1 h1:o0+MgICZLuZ7xjH7Vx6zS/zcu93/BEp1VwkIW1mEXCE= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= -github.com/onsi/gomega v1.10.4 h1:NiTx7EEvBzu9sFOD1zORteLSt3o8gnlvZZwSE9TnY9U= -github.com/onsi/gomega v1.10.4/go.mod h1:g/HbgYopi++010VEqkFgJHKC09uJiW9UkXvMUuKHUCQ= +github.com/onsi/gomega v1.14.0 h1:ep6kpPVwmr/nTbklSx2nrLNSIO62DoYAhnPNIMhK8gI= +github.com/onsi/gomega v1.14.0/go.mod h1:cIuvLEne0aoVhAgh/O6ac0Op8WWw9H6eYCriF+tEHG0= github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -183,13 +174,15 @@ github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTd github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= @@ -197,10 +190,10 @@ go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -230,6 +223,7 @@ golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -240,7 +234,6 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= -golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -251,12 +244,11 @@ golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7 h1:AeiKBIuRw3UomYXSbLy0Mc2dDLfdtbT/IVn4keq83P0= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20201110031124-69a78807bb2b h1:uwuIcX0g4Yl1NC5XAz37xsr2lTtcqevgzYNVt49waME= -golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb h1:eBmm0M9fYhWpKZLjQUUKka/LtIxf46G4fxeEz5KJr9U= -golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= +golang.org/x/net v0.0.0-20210520170846-37e1c6afe023 h1:ADo5wSpq2gqaCGQWzk7S5vd//0iyyLeAratkEoG5dLE= +golang.org/x/net v0.0.0-20210520170846-37e1c6afe023/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -268,6 +260,7 @@ golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -276,13 +269,12 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e h1:N7DeIrjYszNmSW409R3frPPwglRwMkXSBzwVbkOjLLA= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -293,27 +285,28 @@ golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200519105757-fe76b779f299 h1:DYfZAGf2WMFjMxbgTjaC+2HC7NkNAQs+6Q8b9WEB/F4= golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f h1:+Nyd8tzPX9R7BWHguqsrbFdRx3WQ/1ib8I44HXV5yTA= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201112073958-5cba982894dd h1:5CtCZbICpIOFdgO940moixOPjc0178IU44m4EjOO5IY= -golang.org/x/sys v0.0.0-20201112073958-5cba982894dd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22 h1:RqytpXGR1iVNX7psjB3ff8y7sNFinVFvkx1c8SjBkio= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= -golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.4 h1:0YWbFKbhXG/wIiuHDSKpS0Iy7FSA+u45VtBMfQcFTTc= -golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= @@ -323,7 +316,6 @@ golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBn golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= @@ -344,9 +336,11 @@ golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapK golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -384,6 +378,7 @@ google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfG google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20201019141844-1ed22bb0c154/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= @@ -397,18 +392,18 @@ google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQ google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c= -google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= @@ -416,40 +411,38 @@ gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkep gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -k8s.io/api v0.20.2 h1:y/HR22XDZY3pniu9hIFDLpUCPq2w5eQ6aV/VFQ7uJMw= -k8s.io/api v0.20.2/go.mod h1:d7n6Ehyzx+S+cE3VhTGfVNNqtGc/oL9DCdYYahlurV8= -k8s.io/apimachinery v0.20.2 h1:hFx6Sbt1oG0n6DZ+g4bFt5f6BoMkOjKWsQFu077M3Vg= -k8s.io/apimachinery v0.20.2/go.mod h1:WlLqWAHZGg07AeltaI0MV5uk1Omp8xaN0JGLY6gkRpU= -k8s.io/client-go v0.20.2 h1:uuf+iIAbfnCSw8IGAv/Rg0giM+2bOzHLOsbbrwrdhNQ= -k8s.io/client-go v0.20.2/go.mod h1:kH5brqWqp7HDxUFKoEgiI4v8G1xzbe9giaCenUWJzgE= +k8s.io/api v0.22.0 h1:elCpMZ9UE8dLdYxr55E06TmSeji9I3KH494qH70/y+c= +k8s.io/api v0.22.0/go.mod h1:0AoXXqst47OI/L0oGKq9DG61dvGRPXs7X4/B7KyjBCU= +k8s.io/apimachinery v0.22.0 h1:CqH/BdNAzZl+sr3tc0D3VsK3u6ARVSo3GWyLmfIjbP0= +k8s.io/apimachinery v0.22.0/go.mod h1:O3oNtNadZdeOMxHFVxOreoznohCpy0z6mocxbZr7oJ0= +k8s.io/client-go v0.22.0 h1:sD6o9O6tCwUKCENw8v+HFsuAbq2jCu8cWC61/ydwA50= +k8s.io/client-go v0.22.0/go.mod h1:GUjIuXR5PiEv/RVK5OODUsm6eZk7wtSWZSaSJbpFdGg= k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= -k8s.io/klog/v2 v2.4.0 h1:7+X0fUguPyrKEC4WjH8iGDg3laWgMo5tMnRTIGTTxGQ= -k8s.io/klog/v2 v2.4.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= -k8s.io/kube-openapi v0.0.0-20201113171705-d219536bb9fd/go.mod h1:WOJ3KddDSol4tAGcJo0Tvi+dK12EcqSLqcWsryKMpfM= -k8s.io/utils v0.0.0-20201110183641-67b214c5f920/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= +k8s.io/klog/v2 v2.9.0 h1:D7HV+n1V57XeZ0m6tdRkfknthUaM06VFbWldOFh8kzM= +k8s.io/klog/v2 v2.9.0/go.mod h1:hy9LJ/NvuK+iVyP4Ehqva4HxZG/oXyIS3n3Jmire4Ec= +k8s.io/kube-openapi v0.0.0-20210421082810-95288971da7e/go.mod h1:vHXdDvt9+2spS2Rx9ql3I8tycm3H9FDfdUoIuKCefvw= +k8s.io/utils v0.0.0-20210707171843-4b05e18ac7d9/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= -sigs.k8s.io/structured-merge-diff/v4 v4.0.2 h1:YHQV7Dajm86OuqnIR6zAelnDWBRjo+YhYV9PmGrh1s8= sigs.k8s.io/structured-merge-diff/v4 v4.0.2/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= -sigs.k8s.io/yaml v1.1.0 h1:4A07+ZFc2wgJwo8YNlQpr1rVlgUDlxXHhPJciaPY5gs= -sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= +sigs.k8s.io/structured-merge-diff/v4 v4.1.2 h1:Hr/htKFmJEbtMgS/UD0N+gtgctAqz81t3nu+sPzynno= +sigs.k8s.io/structured-merge-diff/v4 v4.1.2/go.mod h1:j/nl6xW8vLS49O8YvXW1ocPhZawJtm+Yrr7PPRQ0Vg4= sigs.k8s.io/yaml v1.2.0 h1:kr/MCeFWJWTwyaHoR9c8EjH9OumOmoF9YGiZd7lFm/Q= sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= diff --git a/k8s/matchers/uaa_config_structs.go b/k8s/matchers/uaa_config_structs.go index f8ba7e1c369..303f6262c26 100644 --- a/k8s/matchers/uaa_config_structs.go +++ b/k8s/matchers/uaa_config_structs.go @@ -61,10 +61,11 @@ type Database struct { } type Smtp struct { - Host string `yaml:"host"` - Port string `yaml:"port"` - Starttls string `yaml:"starttls"` - FromAddress string `yaml:"from_address"` + Host string `yaml:"host"` + Port string `yaml:"port"` + Starttls string `yaml:"starttls"` + FromAddress string `yaml:"from_address"` + Sslprotocols string `yaml:"sslprotocols"` } type OauthClient struct { diff --git a/k8s/templates/uaa.lib.yml b/k8s/templates/uaa.lib.yml index 019ac2f4f1c..b0de9966a49 100644 --- a/k8s/templates/uaa.lib.yml +++ b/k8s/templates/uaa.lib.yml @@ -23,6 +23,7 @@ smtp: port: #@ data.values.smtp.port starttls: #@ data.values.smtp.starttls from_address: #@ data.values.smtp.from_address + sslprotocols: #@ data.values.smtp.sslprotocols oauth: client: diff --git a/k8s/templates/values/_values.yml b/k8s/templates/values/_values.yml index e4e04fe5cbd..ee2e9c3ae5e 100644 --- a/k8s/templates/values/_values.yml +++ b/k8s/templates/values/_values.yml @@ -64,6 +64,7 @@ smtp: password: ~ starttls: ~ from_address: ~ + sslprotocols: ~ admin: client_secret: ~ diff --git a/k8s/templates/values/image.yml b/k8s/templates/values/image.yml index c39eb93d24e..030effe72c0 100644 --- a/k8s/templates/values/image.yml +++ b/k8s/templates/values/image.yml @@ -1,3 +1,3 @@ #@data/values --- -image: "index.docker.io/cloudfoundry/uaa@sha256:07acf322ca4d5247709c47ebd181e90185e9f8f029302139aaab99e6a421788e" +image: "index.docker.io/cloudfoundry/uaa@sha256:412b91aebf09c21b149b373de1c0a569e40881895741d463ddede55d18720e74" \ No newline at end of file diff --git a/k8s/test/config_map_test.go b/k8s/test/config_map_test.go index 14f254d99ba..17c188c5f7e 100644 --- a/k8s/test/config_map_test.go +++ b/k8s/test/config_map_test.go @@ -130,6 +130,7 @@ logger.cfIdentity.appenderRef.uaaDefaultAppender.ref = UaaDefaultAppender` "smtp.port": "smtp port", "smtp.starttls": "smtp starttls", "smtp.from_address": "smtp from_address", + "smtp.sslprotocols": "smtp sslprotocols", "issuer.uri": "http://some.example.com/with/path", }) @@ -145,10 +146,11 @@ logger.cfIdentity.appenderRef.uaaDefaultAppender.ref = UaaDefaultAppender` "Url": Equal("any other database connection string"), }), "Smtp": MatchFields(IgnoreExtras, Fields{ - "Host": Equal("smtp host"), - "Port": Equal("smtp port"), - "Starttls": Equal("smtp starttls"), - "FromAddress": Equal("smtp from_address"), + "Host": Equal("smtp host"), + "Port": Equal("smtp port"), + "Starttls": Equal("smtp starttls"), + "Sslprotocols": Equal("smtp sslprotocols"), + "FromAddress": Equal("smtp from_address"), }), }) }), diff --git a/model/pom.xml b/model/pom.xml index e4628036509..9cf5fa343c1 100644 --- a/model/pom.xml +++ b/model/pom.xml @@ -95,7 +95,7 @@ commons-io commons-io - 2.7 + 2.11.0 compile @@ -252,7 +252,7 @@ org.springframework.security.oauth spring-security-oauth2 - 2.4.0.RELEASE + 2.5.1.RELEASE compile diff --git a/model/src/main/java/org/cloudfoundry/identity/uaa/account/OpenIdConfiguration.java b/model/src/main/java/org/cloudfoundry/identity/uaa/account/OpenIdConfiguration.java index 655d1f5bf2a..261a393bf5b 100644 --- a/model/src/main/java/org/cloudfoundry/identity/uaa/account/OpenIdConfiguration.java +++ b/model/src/main/java/org/cloudfoundry/identity/uaa/account/OpenIdConfiguration.java @@ -60,6 +60,9 @@ public class OpenIdConfiguration { @JsonProperty("ui_locales_supported") private String[] uiLocalesSupported = new String[]{"en-US"}; + @JsonProperty("code_challenge_methods_supported") + private String[] codeChallengeMethodsSupported = new String[]{"S256", "plain"}; + public OpenIdConfiguration(final String contextPath, final String issuer) { this.issuer = issuer; this.authUrl = contextPath + "/oauth/authorize"; diff --git a/model/src/main/java/org/cloudfoundry/identity/uaa/oauth/token/ClaimConstants.java b/model/src/main/java/org/cloudfoundry/identity/uaa/oauth/token/ClaimConstants.java index b040543d834..ccee9736ef5 100644 --- a/model/src/main/java/org/cloudfoundry/identity/uaa/oauth/token/ClaimConstants.java +++ b/model/src/main/java/org/cloudfoundry/identity/uaa/oauth/token/ClaimConstants.java @@ -29,7 +29,7 @@ public class ClaimConstants { public static final String EMAIL = "email"; public static final String EMAIL_VERIFIED = "email_verified"; public static final String CLIENT_ID = "client_id"; - public static final String EXP = "exp"; + public static final String EXPIRY_IN_SECONDS = "exp"; public static final String AUTHORITIES = "authorities"; public static final String SCOPE = "scope"; public static final String GRANTED_SCOPES = "granted_scopes"; diff --git a/model/src/main/java/org/cloudfoundry/identity/uaa/oauth/token/Claims.java b/model/src/main/java/org/cloudfoundry/identity/uaa/oauth/token/Claims.java index fbbebf87318..641602220f8 100644 --- a/model/src/main/java/org/cloudfoundry/identity/uaa/oauth/token/Claims.java +++ b/model/src/main/java/org/cloudfoundry/identity/uaa/oauth/token/Claims.java @@ -41,7 +41,7 @@ public class Claims { private String email; @JsonProperty(ClaimConstants.CLIENT_ID) private String clientId; - @JsonProperty(ClaimConstants.EXP) + @JsonProperty(ClaimConstants.EXPIRY_IN_SECONDS) private Long exp; @JsonProperty(ClaimConstants.AUTHORITIES) private List authorities; diff --git a/model/src/main/java/org/cloudfoundry/identity/uaa/provider/AbstractExternalOAuthIdentityProviderDefinition.java b/model/src/main/java/org/cloudfoundry/identity/uaa/provider/AbstractExternalOAuthIdentityProviderDefinition.java index 62df5728e7f..0e09e866906 100644 --- a/model/src/main/java/org/cloudfoundry/identity/uaa/provider/AbstractExternalOAuthIdentityProviderDefinition.java +++ b/model/src/main/java/org/cloudfoundry/identity/uaa/provider/AbstractExternalOAuthIdentityProviderDefinition.java @@ -28,6 +28,7 @@ public abstract class AbstractExternalOAuthIdentityProviderDefinition implements Cloneable { - private URL userInfoUrl; private URL discoveryUrl; private boolean passwordGrantEnabled = false; private boolean setForwardHeader = false; @JsonInclude(JsonInclude.Include.NON_NULL) private List prompts = null; - public URL getUserInfoUrl() { - return userInfoUrl; - } - - public OIDCIdentityProviderDefinition setUserInfoUrl(URL userInfoUrl) { - this.userInfoUrl = userInfoUrl; - return this; - } - public URL getDiscoveryUrl() { return discoveryUrl; } @@ -85,7 +75,6 @@ public boolean equals(Object o) { OIDCIdentityProviderDefinition that = (OIDCIdentityProviderDefinition) o; - if (!Objects.equals(userInfoUrl, that.userInfoUrl)) return false; if (this.passwordGrantEnabled != that.passwordGrantEnabled) return false; if (this.setForwardHeader != that.setForwardHeader) return false; return Objects.equals(discoveryUrl, that.discoveryUrl); @@ -95,7 +84,6 @@ public boolean equals(Object o) { @Override public int hashCode() { int result = super.hashCode(); - result = 31 * result + (userInfoUrl != null ? userInfoUrl.hashCode() : 0); result = 31 * result + (discoveryUrl != null ? discoveryUrl.hashCode() : 0); result = 31 * result + (passwordGrantEnabled ? 1 : 0); result = 31 * result + (setForwardHeader ? 1 : 0); diff --git a/model/src/test/java/org/cloudfoundry/identity/uaa/account/OpenIdConfigurationTests.java b/model/src/test/java/org/cloudfoundry/identity/uaa/account/OpenIdConfigurationTests.java index e3f0e30cc33..76482dbf243 100644 --- a/model/src/test/java/org/cloudfoundry/identity/uaa/account/OpenIdConfigurationTests.java +++ b/model/src/test/java/org/cloudfoundry/identity/uaa/account/OpenIdConfigurationTests.java @@ -49,6 +49,7 @@ void defaultClaims() { assertFalse(defaultConfig.isClaimsParameterSupported()); assertEquals("http://docs.cloudfoundry.org/api/uaa/", defaultConfig.getServiceDocumentation()); assertArrayEquals(new String[]{"en-US"}, defaultConfig.getUiLocalesSupported()); + assertArrayEquals(new String[]{"S256", "plain"}, defaultConfig.getCodeChallengeMethodsSupported()); } @Test diff --git a/model/src/test/resources/org/cloudfoundry/identity/uaa/account/OpenIdConfiguration-nulls.json b/model/src/test/resources/org/cloudfoundry/identity/uaa/account/OpenIdConfiguration-nulls.json index d718d456363..246b027d5a4 100644 --- a/model/src/test/resources/org/cloudfoundry/identity/uaa/account/OpenIdConfiguration-nulls.json +++ b/model/src/test/resources/org/cloudfoundry/identity/uaa/account/OpenIdConfiguration-nulls.json @@ -15,5 +15,6 @@ "claims_supported": null, "claims_parameter_supported": false, "service_documentation": null, - "ui_locales_supported": null + "ui_locales_supported": null, + "code_challenge_methods_supported": null } \ No newline at end of file diff --git a/model/src/test/resources/org/cloudfoundry/identity/uaa/account/OpenIdConfiguration.json b/model/src/test/resources/org/cloudfoundry/identity/uaa/account/OpenIdConfiguration.json index 4f9f11cc99f..00877d9d283 100644 --- a/model/src/test/resources/org/cloudfoundry/identity/uaa/account/OpenIdConfiguration.json +++ b/model/src/test/resources/org/cloudfoundry/identity/uaa/account/OpenIdConfiguration.json @@ -68,5 +68,9 @@ "service_documentation": "http://docs.cloudfoundry.org/api/uaa/", "ui_locales_supported": [ "en-US" + ], + "code_challenge_methods_supported": [ + "S256", + "plain" ] } \ No newline at end of file diff --git a/samples/api/pom.xml b/samples/api/pom.xml index 43f85a5d504..d201f66d6ca 100644 --- a/samples/api/pom.xml +++ b/samples/api/pom.xml @@ -142,7 +142,7 @@ org.apache.tomcat.embed tomcat-embed-core - 9.0.37 + 9.0.45 provided diff --git a/samples/app/pom.xml b/samples/app/pom.xml index a4a24df1c2f..712f909c828 100644 --- a/samples/app/pom.xml +++ b/samples/app/pom.xml @@ -56,7 +56,7 @@ org.apache.tomcat.embed tomcat-embed-core - 9.0.37 + 9.0.45 provided diff --git a/scripts/start_db_helper.sh b/scripts/start_db_helper.sh index b7dcb37889a..7bcab978619 100755 --- a/scripts/start_db_helper.sh +++ b/scripts/start_db_helper.sh @@ -16,7 +16,8 @@ function bootDB { db=$1 if [[ "${db}" = "postgresql" ]]; then - launchDB="(/docker-entrypoint.sh postgres -c 'max_connections=250' &> /var/log/postgres-boot.log) &" + bootLogLocation="/var/log/postgres-boot.log" + launchDB="(/docker-entrypoint.sh postgres -c 'max_connections=250' &> ${bootLogLocation}) &" testConnection="(! ps aux | grep docker-entrypoint | grep -v 'grep') && psql -h localhost -U postgres -c '\conninfo' &>/dev/null" initDB="psql -c 'drop database if exists uaa;' -U postgres; psql -c 'create database uaa;' -U postgres; psql -c 'drop user if exists root;' --dbname=uaa -U postgres; psql -c \"create user root with superuser password 'changeme';\" --dbname=uaa -U postgres; psql -c 'show max_connections;' --dbname=uaa -U postgres;" @@ -27,7 +28,8 @@ function bootDB { elif [[ "${db}" = "mysql" ]] || [[ "${db}" = "mysql-5.6" ]]; then - launchDB="(MYSQL_DATABASE=uaa MYSQL_ROOT_HOST=127.0.0.1 MYSQL_ROOT_PASSWORD='changeme' bash /entrypoint.sh mysqld &> /var/log/mysql-boot.log) &" + bootLogLocation="/var/log/mysql-boot.log" + launchDB="(MYSQL_DATABASE=uaa MYSQL_ROOT_HOST=127.0.0.1 MYSQL_ROOT_PASSWORD='changeme' bash /entrypoint.sh mysqld &> ${bootLogLocation}) &" testConnection="echo '\s;' | mysql -uroot -pchangeme &>/dev/null" initDB="mysql -uroot -pchangeme -e 'SET GLOBAL max_connections = 250; ALTER DATABASE uaa DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_general_ci;';" @@ -37,7 +39,8 @@ function bootDB { } elif [[ "${db}" = "percona" ]]; then - launchDB="bash /entrypoint.sh &> /var/log/mysql-boot.log" + bootLogLocation="/var/log/mysql-boot.log" + launchDB="bash /entrypoint.sh &> ${bootLogLocation}" testConnection="echo '\s;' | mysql &>/dev/null" initDB="mysql -e \"CREATE USER 'root'@'127.0.0.1' IDENTIFIED BY 'changeme' ;\"; mysql -e \"GRANT ALL ON *.* TO 'root'@'127.0.0.1' WITH GRANT OPTION ;\"; @@ -60,7 +63,9 @@ function bootDB { echo -n "Booting $db" set -x eval "$launchDB" - while true; do + + for i in {0..600} # wait at most 10 mins to the database to start + do set +ex eval "$testConnection" exitcode=$? @@ -69,7 +74,24 @@ function bootDB { set -x echo "Connection established to $db" sleep 1 - eval "$initDB" + + local number_attempts=7 + for attempt in $(seq 1 ${number_attempts}); do + if eval "$initDB"; then + echo 'DB initialized' + break + else + if [[ ${attempt} == ${number_attempts} ]]; then + echo 'error initializing the DB, aborting' + exit 2 + fi + + local wait_time="$((2 ** (attempt - 1)))" + echo "Error initializing the DB, retrying in ${wait_time} seconds" + sleep "${wait_time}" + fi + done + for db_id in `seq 1 $NUM_OF_DATABASES_TO_CREATE`; do createDB $db_id @@ -80,4 +102,8 @@ function bootDB { echo -n "." sleep 1 done + + echo "Printing database boot logs:" + cat "$bootLogLocation" + exit 1 } diff --git a/server/build.gradle b/server/build.gradle index aeb132a4386..d224f28c311 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -79,6 +79,8 @@ dependencies { implementation(libraries.javaxXmlBindCore) implementation(libraries.javaxXmlBindImpl) + implementation(libraries.nimbusJwt) + testImplementation(project(":cloudfoundry-identity-model").sourceSets.test.output) testImplementation(libraries.springTest) @@ -95,6 +97,7 @@ dependencies { testImplementation(libraries.tomcatJdbc) testImplementation(libraries.jsonPathAssert) + testImplementation(libraries.guavaTestLib) } configurations.all { diff --git a/server/pom.xml b/server/pom.xml index 0db5700fae5..eabc6a6be8b 100644 --- a/server/pom.xml +++ b/server/pom.xml @@ -60,7 +60,7 @@ org.apache.tomcat tomcat-jdbc - 9.0.37 + 9.0.50 compile @@ -710,7 +710,7 @@ org.springframework.security.oauth spring-security-oauth2 - 2.4.0.RELEASE + 2.5.0.RELEASE compile @@ -766,7 +766,7 @@ org.bouncycastle bcprov-jdk15on - 1.66 + 1.69 compile @@ -810,7 +810,7 @@ org.bouncycastle bcpkix-jdk15on - 1.66 + 1.69 compile @@ -854,7 +854,7 @@ com.google.guava guava - 29.0-jre + 30.1.1-jre compile @@ -1565,7 +1565,7 @@ org.passay passay - 1.2.0 + 1.6.0 compile @@ -1870,7 +1870,7 @@ org.apache.tomcat.embed tomcat-embed-core - 9.0.37 + 9.0.50 provided @@ -2345,7 +2345,7 @@ org.apache.tomcat tomcat-el-api - 9.0.37 + 9.0.50 test @@ -2389,7 +2389,7 @@ org.apache.tomcat tomcat-jasper-el - 9.0.37 + 9.0.50 test diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/account/UserInfoEndpoint.java b/server/src/main/java/org/cloudfoundry/identity/uaa/account/UserInfoEndpoint.java index d940337f1ae..eb37d02feba 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/account/UserInfoEndpoint.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/account/UserInfoEndpoint.java @@ -1,8 +1,8 @@ package org.cloudfoundry.identity.uaa.account; import org.cloudfoundry.identity.uaa.authentication.UaaPrincipal; -import org.cloudfoundry.identity.uaa.user.UaaUser; import org.cloudfoundry.identity.uaa.user.UaaUserDatabase; +import org.cloudfoundry.identity.uaa.user.UaaUserPrototype; import org.cloudfoundry.identity.uaa.user.UserInfo; import org.springframework.beans.factory.InitializingBean; import org.springframework.security.oauth2.provider.OAuth2Authentication; @@ -53,7 +53,7 @@ protected UaaPrincipal extractUaaPrincipal(OAuth2Authentication authentication) } protected UserInfoResponse getResponse(UaaPrincipal principal, boolean addCustomAttributes, boolean addRoles) { - UaaUser user = userDatabase.retrieveUserById(principal.getId()); + UaaUserPrototype user = userDatabase.retrieveUserPrototypeById(principal.getId()); UserInfoResponse response = new UserInfoResponse(); response.setUserId(user.getId()); response.setUserName(user.getUsername()); diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/authentication/SessionResetFilter.java b/server/src/main/java/org/cloudfoundry/identity/uaa/authentication/SessionResetFilter.java index 5a8b5372f33..c64a761cf41 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/authentication/SessionResetFilter.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/authentication/SessionResetFilter.java @@ -15,10 +15,10 @@ package org.cloudfoundry.identity.uaa.authentication; +import org.cloudfoundry.identity.uaa.user.UaaUserPrototype; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.cloudfoundry.identity.uaa.constants.OriginKeys; -import org.cloudfoundry.identity.uaa.user.UaaUser; import org.cloudfoundry.identity.uaa.user.UaaUserDatabase; import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; @@ -65,7 +65,7 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse String userId = authentication.getPrincipal().getId(); try { logger.debug("Evaluating user-id for session reset:"+userId); - UaaUser user = userDatabase.retrieveUserById(userId); + UaaUserPrototype user = userDatabase.retrieveUserPrototypeById(userId); Date lastModified; if ((lastModified = user.getPasswordLastModified()) != null) { long lastAuthTime = authentication.getAuthenticatedTime(); diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/authentication/UaaPrincipal.java b/server/src/main/java/org/cloudfoundry/identity/uaa/authentication/UaaPrincipal.java index 9dc03cbb5e7..d67acf46b97 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/authentication/UaaPrincipal.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/authentication/UaaPrincipal.java @@ -18,6 +18,7 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; import org.cloudfoundry.identity.uaa.user.UaaUser; +import org.cloudfoundry.identity.uaa.user.UaaUserPrototype; /** * The principal object which should end up as the representation of an @@ -45,6 +46,16 @@ public UaaPrincipal(UaaUser user) { ); } + public UaaPrincipal(UaaUserPrototype userPrototype) { + this( + userPrototype.getId(), + userPrototype.getUsername(), + userPrototype.getEmail(), + userPrototype.getOrigin(), + userPrototype.getExternalId(), + userPrototype.getZoneId() + ); + } @JsonCreator public UaaPrincipal( @JsonProperty("id") String id, diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/cache/ExpiringUrlCache.java b/server/src/main/java/org/cloudfoundry/identity/uaa/cache/ExpiringUrlCache.java deleted file mode 100644 index 4a986902fee..00000000000 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/cache/ExpiringUrlCache.java +++ /dev/null @@ -1,111 +0,0 @@ -package org.cloudfoundry.identity.uaa.cache; - -import com.google.common.base.Ticker; -import com.google.common.cache.Cache; -import com.google.common.cache.CacheBuilder; -import org.cloudfoundry.identity.uaa.util.TimeService; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpEntity; -import org.springframework.http.HttpMethod; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.stereotype.Component; -import org.springframework.web.client.RestClientException; -import org.springframework.web.client.RestTemplate; - -import java.net.URI; -import java.net.URISyntaxException; -import java.time.Duration; -import java.time.Instant; -import java.util.concurrent.TimeUnit; - -@Component -public class ExpiringUrlCache implements UrlContentCache { - private static final Logger logger = LoggerFactory.getLogger(ExpiringUrlCache.class); - private static final int DEFAULT_MAX_ENTRIES = 10_000; - - private final Duration cacheExpiration; - private final TimeService timeService; - private final Cache cache; - - @Autowired - public ExpiringUrlCache(final TimeService timeService) { - this(Duration.ofMinutes(10), timeService, DEFAULT_MAX_ENTRIES); - } - - public ExpiringUrlCache( - final Duration cacheExpiration, - final TimeService timeService, - final int maxEntries) { - this.cacheExpiration = cacheExpiration; - this.timeService = timeService; - this.cache = CacheBuilder - .newBuilder() - .expireAfterWrite(this.cacheExpiration.toMillis(), TimeUnit.MILLISECONDS) - .maximumSize(maxEntries) - .ticker(Ticker.systemTicker()) - .build(); - } - - @Override - public byte[] getUrlContent(String uri, final RestTemplate template) { - return getUrlContent(uri, template, HttpMethod.GET, null); - } - - @Override - public byte[] getUrlContent(String uri, final RestTemplate template, final HttpMethod method, HttpEntity requestEntity) { - try { - final URI netUri = new URI(uri); - CacheEntry entry = cache.getIfPresent(uri); - byte[] metadata = entry != null ? entry.data : null; - if (metadata == null || isEntryExpired(entry)) { - logger.debug("Fetching metadata for " + uri); - if (requestEntity != null) { - ResponseEntity responseEntity = template.exchange(netUri, method, requestEntity, byte[].class); - if (responseEntity.getStatusCode() == HttpStatus.OK) { - metadata = responseEntity.getBody(); - } else { - throw new IllegalArgumentException("Unable to fetch content, status:" + responseEntity.getStatusCode().getReasonPhrase()); - } - } else { - metadata = template.getForObject(netUri, byte[].class); - } - Instant now = Instant.ofEpochMilli(timeService.getCurrentTimeMillis()); - cache.put(uri, new CacheEntry(now, metadata)); - } - return metadata; - } catch (RestClientException x) { - logger.warn("Unable to fetch metadata for {0}. {1}", uri, x.getMessage()); - throw x; - } catch (URISyntaxException e) { - throw new IllegalArgumentException(e); - } - } - - private boolean isEntryExpired(CacheEntry entry) { - Instant now = Instant.ofEpochMilli(timeService.getCurrentTimeMillis()); - return Duration.between(entry.timeEntered, now).compareTo(cacheExpiration) > 0; - } - - @Override - public void clear() { - cache.invalidateAll(); - } - - @Override - public long size() { - return cache.size(); - } - - static class CacheEntry { - final Instant timeEntered; - final byte[] data; - - CacheEntry(Instant timeEntered, byte[] data) { - this.timeEntered = timeEntered; - this.data = data; - } - } -} diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/cache/StaleUrlCache.java b/server/src/main/java/org/cloudfoundry/identity/uaa/cache/StaleUrlCache.java new file mode 100644 index 00000000000..8698b747e2e --- /dev/null +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/cache/StaleUrlCache.java @@ -0,0 +1,172 @@ +package org.cloudfoundry.identity.uaa.cache; + +import java.net.URI; +import java.net.URISyntaxException; +import java.time.Duration; +import java.time.Instant; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; + +import com.google.common.base.Ticker; +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; +import com.google.common.util.concurrent.UncheckedExecutionException; + +import org.cloudfoundry.identity.uaa.util.TimeService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestClientException; +import org.springframework.web.client.RestTemplate; + +@Component +public class StaleUrlCache implements UrlContentCache { + private static final Logger logger = LoggerFactory.getLogger(StaleUrlCache.class); + private static final int DEFAULT_MAX_ENTRIES = 10_000; + + private final Duration cacheExpiration; + private final LoadingCache cache; + + @Autowired + public StaleUrlCache(final TimeService timeService) { + this(timeService, Ticker.systemTicker()); + } + + public StaleUrlCache(final TimeService timeService, final Ticker ticker) { + this(Duration.ofMinutes(10), timeService, DEFAULT_MAX_ENTRIES, ticker); + } + + public StaleUrlCache(final Duration cacheExpiration, final TimeService timeService, final int maxEntries, + final Ticker ticker) { + this.cacheExpiration = cacheExpiration; + this.cache = CacheBuilder.newBuilder().refreshAfterWrite(this.cacheExpiration.toMillis(), TimeUnit.MILLISECONDS) + .maximumSize(maxEntries).ticker(ticker).build(new UrlCacheLoader(timeService)); + } + + @Override + public byte[] getUrlContent(String uri, final RestTemplate template) { + return getUrlContent(uri, template, HttpMethod.GET, null); + } + + @Override + public byte[] getUrlContent(String uri, final RestTemplate template, final HttpMethod method, + HttpEntity requestEntity) { + try { + return cache.get(new UriRequest(uri, template, method, requestEntity)).data; + } catch (UncheckedExecutionException e) { + logger.warn("UncheckedException " + e.getMessage() + e); + throw (RuntimeException) e.getCause(); + } catch (ExecutionException e) { + logger.warn("ExecutionException " + e.getMessage() + e); + throw new IllegalArgumentException(e); + } + } + + @Override + public void clear() { + cache.invalidateAll(); + } + + @Override + public long size() { + return cache.size(); + } + + static class UriRequest { + final String uri; + final RestTemplate template; + final HttpMethod method; + final HttpEntity requestEntity; + + UriRequest(String uri, RestTemplate template, HttpMethod method, HttpEntity requestEntity) { + this.uri = uri; + this.template = template; + this.method = method; + this.requestEntity = requestEntity; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((uri == null) ? 0 : uri.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + UriRequest other = (UriRequest) obj; + if (uri == null) { + if (other.uri != null) { + return false; + } + } else if (!uri.equals(other.uri)) { + return false; + } + return true; + } + } + + static class CacheEntry { + final Instant timeEntered; + final byte[] data; + + CacheEntry(Instant timeEntered, byte[] data) { + this.timeEntered = timeEntered; + this.data = data; + } + + } + + class UrlCacheLoader extends CacheLoader { + + private final TimeService timeService; + + UrlCacheLoader(TimeService timeService) { + this.timeService = timeService; + } + + @Override + public CacheEntry load(UriRequest request) throws RuntimeException { + try { + byte[] metadata; + final URI netUri = new URI(request.uri); + if (request.requestEntity != null) { + ResponseEntity responseEntity = request.template.exchange(netUri, request.method, + request.requestEntity, byte[].class); + if (responseEntity.getStatusCode() == HttpStatus.OK) { + metadata = responseEntity.getBody(); + } else { + throw new IllegalArgumentException( + "Unable to fetch content, status:" + responseEntity.getStatusCode().getReasonPhrase()); + } + } else { + metadata = request.template.getForObject(netUri, byte[].class); + } + Instant now = Instant.ofEpochMilli(timeService.getCurrentTimeMillis()); + return new CacheEntry(now, metadata); + } catch (RestClientException x) { + logger.warn("Unable to fetch metadata for {0}. {1}", request.uri, x.getMessage()); + throw x; + } catch (URISyntaxException e) { + throw new IllegalArgumentException(e); + } + } + + } +} diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/codestore/ExpiringCodeStore.java b/server/src/main/java/org/cloudfoundry/identity/uaa/codestore/ExpiringCodeStore.java index ed19be246e0..22ac65cfe8c 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/codestore/ExpiringCodeStore.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/codestore/ExpiringCodeStore.java @@ -30,6 +30,19 @@ public interface ExpiringCodeStore { */ ExpiringCode generateCode(String data, Timestamp expiresAt, String intent, String zoneId); + /** + * Retrieve a code BUT DO NOT DELETE IT. + * + * WARNING - if you intend to expire the code as soon as you read it, + * use {@link #retrieveCode(String, String)} instead. + * + * @param code the one-time code to look for + * @param zoneId + * @return code or null if the code is not found + * @throws java.lang.NullPointerException if the code is null + */ + ExpiringCode peekCode(String code, String zoneId); + /** * Retrieve a code and delete it if it exists. * diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/codestore/JdbcExpiringCodeStore.java b/server/src/main/java/org/cloudfoundry/identity/uaa/codestore/JdbcExpiringCodeStore.java index 27a5b9e8788..79fa9a4e05f 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/codestore/JdbcExpiringCodeStore.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/codestore/JdbcExpiringCodeStore.java @@ -111,6 +111,25 @@ public ExpiringCode generateCode(String data, Timestamp expiresAt, String intent return null; } + @Override + public ExpiringCode peekCode(String code, String zoneId) { + cleanExpiredEntries(); + + if (code == null) { + throw new NullPointerException(); + } + + try { + ExpiringCode expiringCode = jdbcTemplate.queryForObject(selectAllFields, rowMapper, code, zoneId); + if (expiringCode.getExpiresAt().getTime() < timeService.getCurrentTimeMillis()) { + expiringCode = null; + } + return expiringCode; + } catch (EmptyResultDataAccessException x) { + return null; + } + } + @Override public ExpiringCode retrieveCode(String code, String zoneId) { cleanExpiredEntries(); diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/invitations/InvitationsController.java b/server/src/main/java/org/cloudfoundry/identity/uaa/invitations/InvitationsController.java index 94fa0587d79..8df3b94b410 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/invitations/InvitationsController.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/invitations/InvitationsController.java @@ -116,7 +116,7 @@ public void return404(HttpServletResponse response) { @RequestMapping(value = "/accept", method = GET, params = {"code"}) public String acceptInvitePage(@RequestParam String code, Model model, HttpServletRequest request, HttpServletResponse response) { - ExpiringCode expiringCode = expiringCodeStore.retrieveCode(code, IdentityZoneHolder.get().getId()); + ExpiringCode expiringCode = expiringCodeStore.peekCode(code, IdentityZoneHolder.get().getId()); if ((null == expiringCode) || (null != expiringCode.getIntent() && !INVITATION.name().equals(expiringCode.getIntent()))) { return handleUnprocessableEntity(model, response, "error_message_code", "code_expired", "invitations/accept_invite"); } @@ -128,7 +128,6 @@ public String acceptInvitePage(@RequestParam String code, Model model, HttpServl String origin = codeData.get(ORIGIN); try { IdentityProvider provider = identityProviderProvisioning.retrieveByOrigin(origin, IdentityZoneHolder.get().getId()); - final String newCode = expiringCodeStore.generateCode(expiringCode.getData(), new Timestamp(System.currentTimeMillis() + (10 * 60 * 1000)), expiringCode.getIntent(), IdentityZoneHolder.get().getId()).getCode(); UaaUser user = userDatabase.retrieveUserById(codeData.get("user_id")); boolean isUaaUserAndVerified = @@ -136,12 +135,12 @@ public String acceptInvitePage(@RequestParam String code, Model model, HttpServl boolean isExternalUserAndAcceptedInvite = !UAA.equals(provider.getType()) && UaaHttpRequestUtils.isAcceptedInvitationAuthentication(); if (isUaaUserAndVerified || isExternalUserAndAcceptedInvite) { - AcceptedInvitation accepted = invitationsService.acceptInvitation(newCode, ""); + AcceptedInvitation accepted = invitationsService.acceptInvitation(code, ""); String redirect = "redirect:" + accepted.getRedirectUri(); logger.debug(String.format("Redirecting accepted invitation for email:%s, id:%s to URL:%s", codeData.get("email"), codeData.get("user_id"), redirect)); return redirect; } else if (SAML.equals(provider.getType())) { - setRequestAttributes(request, newCode, user); + setRequestAttributes(request, code, user); SamlIdentityProviderDefinition definition = ObjectUtils.castInstance(provider.getConfig(), SamlIdentityProviderDefinition.class); @@ -149,7 +148,7 @@ public String acceptInvitePage(@RequestParam String code, Model model, HttpServl logger.debug(String.format("Redirecting invitation for email:%s, id:%s single SAML IDP URL:%s", codeData.get("email"), codeData.get("user_id"), redirect)); return redirect; } else if (OIDC10.equals(provider.getType()) || OAUTH20.equals(provider.getType())) { - setRequestAttributes(request, newCode, user); + setRequestAttributes(request, code, user); AbstractExternalOAuthIdentityProviderDefinition definition = ObjectUtils.castInstance(provider.getConfig(), AbstractExternalOAuthIdentityProviderDefinition.class); @@ -162,7 +161,7 @@ public String acceptInvitePage(@RequestParam String code, Model model, HttpServl Collections.singletonList(UaaAuthority.UAA_INVITED)); SecurityContextHolder.getContext().setAuthentication(token); model.addAttribute("provider", provider.getType()); - model.addAttribute("code", newCode); + model.addAttribute("code", code); model.addAttribute("email", codeData.get("email")); logger.debug(String.format("Sending user to accept invitation page email:%s, id:%s", codeData.get("email"), codeData.get("user_id"))); } diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/login/LoginInfoEndpoint.java b/server/src/main/java/org/cloudfoundry/identity/uaa/login/LoginInfoEndpoint.java index 4a8042215b0..e07861ab5df 100755 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/login/LoginInfoEndpoint.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/login/LoginInfoEndpoint.java @@ -70,10 +70,12 @@ import java.awt.*; import java.io.IOException; import java.net.URLDecoder; +import java.net.URLEncoder; import java.security.Principal; import java.sql.Timestamp; import java.text.SimpleDateFormat; import java.time.Duration; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Date; @@ -273,15 +275,63 @@ private String login(Model model, Principal principal, List excludedProm clientName = (String) clientInfo.get(ClientConstants.CLIENT_NAME); } - Map samlIdentityProviders = - getSamlIdentityProviderDefinitions(allowedIdentityProviderKeys); - Map oauthIdentityProviders = - getOauthIdentityProviderDefinitions(allowedIdentityProviderKeys); - Map allIdentityProviders = - new HashMap<>() {{ - putAll(samlIdentityProviders); - putAll(oauthIdentityProviders); - }}; + //Read all configuration and parameters at the beginning to allow earlier decisions + boolean discoveryEnabled = IdentityZoneHolder.get().getConfig().isIdpDiscoveryEnabled(); + boolean discoveryPerformed = Boolean.parseBoolean(request.getParameter("discoveryPerformed")); + String defaultIdentityProviderName = IdentityZoneHolder.get().getConfig().getDefaultIdentityProvider(); + if (defaultIdentityProviderName != null) { + model.addAttribute("defaultIdpName", defaultIdentityProviderName); + } + boolean accountChooserEnabled = IdentityZoneHolder.get().getConfig().isAccountChooserEnabled(); + boolean otherAccountSignIn = Boolean.parseBoolean(request.getParameter("otherAccountSignIn")); + boolean savedAccountsEmpty = getSavedAccounts(request.getCookies(), SavedAccountOption.class).isEmpty(); + boolean accountChooserNeeded = accountChooserEnabled + && !(otherAccountSignIn || savedAccountsEmpty) + && !discoveryPerformed; + boolean newLoginPageEnabled = accountChooserEnabled || discoveryEnabled; + + + String loginHintParam = extractLoginHintParam(session, request); + UaaLoginHint uaaLoginHint = UaaLoginHint.parseRequestParameter(loginHintParam); + + Map samlIdentityProviders; + Map oauthIdentityProviders; + Map allIdentityProviders = Collections.emptyMap(); + Map loginHintProviders = Collections.emptyMap(); + + if (uaaLoginHint != null && (allowedIdentityProviderKeys == null || allowedIdentityProviderKeys.contains(uaaLoginHint.getOrigin()))) { + // Login hint: Only try to read the hinted IdP from database + if (!(OriginKeys.UAA.equals(uaaLoginHint.getOrigin()) || OriginKeys.LDAP.equals(uaaLoginHint.getOrigin()))) { + try { + IdentityProvider loginHintProvider = externalOAuthProviderConfigurator + .retrieveByOrigin(uaaLoginHint.getOrigin(), IdentityZoneHolder.get().getId()); + loginHintProviders = Collections.singletonList(loginHintProvider).stream().collect( + new MapCollector( + IdentityProvider::getOriginKey, IdentityProvider::getConfig)); + } catch (EmptyResultDataAccessException ignored) { + } + } + if (!loginHintProviders.isEmpty()) { + oauthIdentityProviders = Collections.emptyMap(); + samlIdentityProviders = Collections.emptyMap(); + } else { + accountChooserNeeded = false; + samlIdentityProviders = getSamlIdentityProviderDefinitions(allowedIdentityProviderKeys); + oauthIdentityProviders = getOauthIdentityProviderDefinitions(allowedIdentityProviderKeys); + allIdentityProviders = new HashMap<>(); + allIdentityProviders.putAll(samlIdentityProviders); + allIdentityProviders.putAll(oauthIdentityProviders); + } + } else if (!jsonResponse && (accountChooserNeeded || (accountChooserEnabled && !discoveryEnabled && !discoveryPerformed))) { + // when `/login` is requested to return html response (as opposed to json response) + //Account and origin chooser do not need idp information + oauthIdentityProviders = Collections.emptyMap(); + samlIdentityProviders = Collections.emptyMap(); + } else { + samlIdentityProviders = getSamlIdentityProviderDefinitions(allowedIdentityProviderKeys); + oauthIdentityProviders = getOauthIdentityProviderDefinitions(allowedIdentityProviderKeys); + allIdentityProviders = new HashMap<>() {{putAll(samlIdentityProviders);putAll(oauthIdentityProviders);}}; + } boolean fieldUsernameShow = true; boolean returnLoginPrompts = true; @@ -311,15 +361,11 @@ private String login(Model model, Principal principal, List excludedProm } Map.Entry idpForRedirect; - idpForRedirect = evaluateLoginHint(model, session, samlIdentityProviders, - oauthIdentityProviders, allIdentityProviders, allowedIdentityProviderKeys, request); - - boolean discoveryEnabled = IdentityZoneHolder.get().getConfig().isIdpDiscoveryEnabled(); - boolean discoveryPerformed = Boolean.parseBoolean(request.getParameter("discoveryPerformed")); - String defaultIdentityProviderName = IdentityZoneHolder.get().getConfig().getDefaultIdentityProvider(); + idpForRedirect = evaluateLoginHint(model, samlIdentityProviders, + oauthIdentityProviders, allIdentityProviders, allowedIdentityProviderKeys, loginHintParam, uaaLoginHint, loginHintProviders); idpForRedirect = evaluateIdpDiscovery(model, samlIdentityProviders, oauthIdentityProviders, - allIdentityProviders, allowedIdentityProviderKeys, idpForRedirect, discoveryEnabled, discoveryPerformed, defaultIdentityProviderName); + allIdentityProviders, allowedIdentityProviderKeys, idpForRedirect, discoveryPerformed, newLoginPageEnabled, defaultIdentityProviderName); if (idpForRedirect == null && !jsonResponse && !fieldUsernameShow && allIdentityProviders.size() == 1) { idpForRedirect = allIdentityProviders.entrySet().stream().findAny().get(); } @@ -334,7 +380,7 @@ private String login(Model model, Principal principal, List excludedProm } boolean linkCreateAccountShow = fieldUsernameShow; - if (fieldUsernameShow && (allowedIdentityProviderKeys != null) && (!discoveryEnabled || discoveryPerformed)) { + if (fieldUsernameShow && (allowedIdentityProviderKeys != null) && ((!discoveryEnabled && !accountChooserEnabled) || discoveryPerformed)) { if (!allowedIdentityProviderKeys.contains(OriginKeys.UAA)) { linkCreateAccountShow = false; model.addAttribute("login_hint", new UaaLoginHint(OriginKeys.LDAP).toString()); @@ -364,7 +410,7 @@ private String login(Model model, Principal principal, List excludedProm excludedPrompts, returnLoginPrompts); if (principal == null) { - return getUnauthenticatedRedirect(model, request, discoveryEnabled, discoveryPerformed); + return getUnauthenticatedRedirect(model, request, discoveryEnabled, discoveryPerformed, accountChooserNeeded ,accountChooserEnabled); } return "home"; } @@ -373,32 +419,34 @@ private String getUnauthenticatedRedirect( Model model, HttpServletRequest request, boolean discoveryEnabled, - boolean discoveryPerformed + boolean discoveryPerformed, + boolean accountChooserNeeded, + boolean accountChooserEnabled ) { String formRedirectUri = request.getParameter(UaaSavedRequestAwareAuthenticationSuccessHandler.FORM_REDIRECT_PARAMETER); if (hasText(formRedirectUri)) { model.addAttribute(UaaSavedRequestAwareAuthenticationSuccessHandler.FORM_REDIRECT_PARAMETER, formRedirectUri); } - - boolean accountChooserEnabled = IdentityZoneHolder.get().getConfig().isAccountChooserEnabled(); - boolean otherAccountSignIn = Boolean.parseBoolean(request.getParameter("otherAccountSignIn")); - boolean savedAccountsEmpty = getSavedAccounts(request.getCookies(), SavedAccountOption.class).isEmpty(); - + if (accountChooserNeeded) { + return "idp_discovery/account_chooser"; + } if (discoveryEnabled) { + if (!discoveryPerformed) { + return "idp_discovery/email"; + } + return goToPasswordPage(request.getParameter("email"), model); + } + if (accountChooserEnabled) { if (model.containsAttribute("login_hint")) { return goToPasswordPage(request.getParameter("email"), model); } - boolean accountChooserNeeded = accountChooserEnabled - && !(otherAccountSignIn || savedAccountsEmpty) - && !discoveryPerformed; - - if (accountChooserNeeded) { + if (model.containsAttribute("error")) { return "idp_discovery/account_chooser"; } - if (!discoveryPerformed) { - return "idp_discovery/email"; + if (discoveryPerformed) { + return goToPasswordPage(request.getParameter("email"), model); } - return goToPasswordPage(request.getParameter("email"), model); + return "idp_discovery/origin"; } return "login"; } @@ -460,11 +508,11 @@ private Map.Entry evaluateIdpDiscove Map allIdentityProviders, List allowedIdentityProviderKeys, Map.Entry idpForRedirect, - boolean discoveryEnabled, boolean discoveryPerformed, + boolean newLoginPageEnabled, String defaultIdentityProviderName ) { - if (idpForRedirect == null && (discoveryPerformed || !discoveryEnabled) && defaultIdentityProviderName != null && !model.containsAttribute("login_hint")) { //Default set, no login_hint given, discovery disabled or performed + if (idpForRedirect == null && (discoveryPerformed || !newLoginPageEnabled) && defaultIdentityProviderName != null && !model.containsAttribute("login_hint") && !model.containsAttribute("error")) { //Default set, no login_hint given, no error, discovery performed if (!OriginKeys.UAA.equals(defaultIdentityProviderName) && !OriginKeys.LDAP.equals(defaultIdentityProviderName)) { if (allIdentityProviders.containsKey(defaultIdentityProviderName)) { idpForRedirect = @@ -480,27 +528,29 @@ private Map.Entry evaluateIdpDiscove return idpForRedirect; } + private String extractLoginHintParam(HttpSession session, HttpServletRequest request) { + String loginHintParam = + ofNullable(session) + .flatMap(s -> ofNullable(SessionUtils.getSavedRequestSession(s))) + .flatMap(sr -> ofNullable(sr.getParameterValues("login_hint"))) + .flatMap(lhValues -> Arrays.stream(lhValues).findFirst()) + .orElse(request.getParameter("login_hint")); + return loginHintParam; + } + private Map.Entry evaluateLoginHint( Model model, - HttpSession session, Map samlIdentityProviders, Map oauthIdentityProviders, Map allIdentityProviders, List allowedIdentityProviderKeys, - HttpServletRequest request + String loginHintParam, + UaaLoginHint uaaLoginHint, + Map loginHintProviders ) { - Map.Entry idpForRedirect = null; - String loginHintParam = - ofNullable(session) - .flatMap(s -> ofNullable(SessionUtils.getSavedRequestSession(s))) - .flatMap(sr -> ofNullable(sr.getParameterValues("login_hint"))) - .flatMap(lhValues -> Arrays.stream(lhValues).findFirst()) - .orElse(request.getParameter("login_hint")); - if (loginHintParam != null) { // parse login_hint in JSON format - UaaLoginHint uaaLoginHint = UaaLoginHint.parseRequestParameter(loginHintParam); if (uaaLoginHint != null) { logger.debug("Received login hint: " + loginHintParam); logger.debug("Received login hint with origin: " + uaaLoginHint.getOrigin()); @@ -519,12 +569,13 @@ private Map.Entry evaluateLoginHint( allIdentityProviders.entrySet().stream().filter( idp -> idp.getKey().equals(uaaLoginHint.getOrigin()) ).collect(Collectors.toList()); - if (hintIdentityProviders.size() > 1) { + if (loginHintProviders.size() > 1) { throw new IllegalStateException( "There is a misconfiguration with the identity provider(s). Please contact your system administrator." ); - } else if (hintIdentityProviders.size() == 1) { - idpForRedirect = hintIdentityProviders.get(0); + } + if (loginHintProviders.size() == 1) { + idpForRedirect = new ArrayList<>(loginHintProviders.entrySet()).get(0); logger.debug("Setting redirect from origin login_hint to: " + idpForRedirect); } else { logger.debug("Client does not allow provider for login_hint with origin key: " @@ -731,6 +782,16 @@ private String extractUrlFromString(String s) { return null; } + @RequestMapping(value = "/origin-chooser", method = RequestMethod.POST) + public String loginUsingOrigin(@RequestParam(required = false, name = "login_hint") String loginHint, Model model, HttpSession session, HttpServletRequest request) { + if (!StringUtils.hasText(loginHint)) { + return "redirect:/login?discoveryPerformed=true"; + } + UaaLoginHint uaaLoginHint = new UaaLoginHint(loginHint); + return "redirect:/login?discoveryPerformed=true&login_hint=" + URLEncoder.encode(uaaLoginHint.toString(), UTF_8); + } + + @RequestMapping(value = "/login/idp_discovery", method = RequestMethod.POST) public String discoverIdentityProvider(@RequestParam String email, @RequestParam(required = false) String skipDiscovery, @RequestParam(required = false, name = "login_hint") String loginHint, @RequestParam(required = false, name = "username") String username,Model model, HttpSession session, HttpServletRequest request) { ClientDetails clientDetails = null; diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/login/ThymeleafConfig.java b/server/src/main/java/org/cloudfoundry/identity/uaa/login/ThymeleafConfig.java index 673230e6472..7efd81c0e43 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/login/ThymeleafConfig.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/login/ThymeleafConfig.java @@ -28,6 +28,7 @@ import org.thymeleaf.spring5.SpringTemplateEngine; import org.thymeleaf.spring5.templateresolver.SpringResourceTemplateResolver; import org.thymeleaf.spring5.view.ThymeleafViewResolver; +import org.thymeleaf.templatemode.TemplateMode; import org.thymeleaf.templateresolver.ITemplateResolver; import java.nio.charset.StandardCharsets; @@ -104,7 +105,7 @@ public org.springframework.web.servlet.view.ContentNegotiatingViewResolver viewR private SpringResourceTemplateResolver baseHtmlTemplateResolver(ApplicationContext context) { SpringResourceTemplateResolver templateResolver = new SpringResourceTemplateResolver(); templateResolver.setSuffix(".html"); - templateResolver.setTemplateMode("HTML5"); + templateResolver.setTemplateMode(TemplateMode.HTML); templateResolver.setApplicationContext(context); return templateResolver; } diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/AuthTimeDateConverter.java b/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/AuthTimeDateConverter.java index 186306874a0..53ed7f2f394 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/AuthTimeDateConverter.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/AuthTimeDateConverter.java @@ -12,9 +12,9 @@ * http://openid.net/specs/openid-connect-core-1_0.html#IDToken */ public class AuthTimeDateConverter { - public static Date authTimeToDate(Integer authTime) { + public static Date authTimeToDate(Long authTime) { if (null != authTime) { - return new Date(authTime.longValue() * 1000l); + return new Date(authTime * 1000l); } return null; } diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/KeyInfo.java b/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/KeyInfo.java index 702ad53bf9d..e4d9cdb97da 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/KeyInfo.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/KeyInfo.java @@ -1,5 +1,6 @@ package org.cloudfoundry.identity.uaa.oauth; +import com.nimbusds.jose.util.Base64URL; import org.bouncycastle.asn1.ASN1Sequence; import org.cloudfoundry.identity.uaa.oauth.jwk.JsonWebKey; import org.cloudfoundry.identity.uaa.oauth.jwt.JwtAlgorithms; @@ -262,9 +263,8 @@ public Map getJwkMap() { RSAPublicKey rsaKey = (RSAPublicKey) parseKeyPair(verifierKey).getPublic(); if (rsaKey != null) { - java.util.Base64.Encoder encoder = java.util.Base64.getUrlEncoder().withoutPadding(); - String n = encoder.encodeToString(rsaKey.getModulus().toByteArray()); - String e = encoder.encodeToString(rsaKey.getPublicExponent().toByteArray()); + String n = Base64URL.encode(rsaKey.getModulus()).toString(); + String e = Base64URL.encode(rsaKey.getPublicExponent()).toString(); result.put("n", n); result.put("e", e); } diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/UaaAuthorizationEndpoint.java b/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/UaaAuthorizationEndpoint.java index b509c0c80dd..89299c73ff7 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/UaaAuthorizationEndpoint.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/UaaAuthorizationEndpoint.java @@ -4,6 +4,7 @@ import org.apache.http.client.utils.URIUtils; import org.cloudfoundry.identity.uaa.authentication.UaaPrincipal; import org.cloudfoundry.identity.uaa.oauth.client.ClientConstants; +import org.cloudfoundry.identity.uaa.oauth.pkce.PkceValidationService; import org.cloudfoundry.identity.uaa.oauth.token.CompositeToken; import org.cloudfoundry.identity.uaa.util.UaaHttpRequestUtils; import org.cloudfoundry.identity.uaa.zone.IdentityZoneHolder; @@ -119,6 +120,7 @@ public class UaaAuthorizationEndpoint extends AbstractEndpoint implements Authen private final SessionAttributeStore sessionAttributeStore; private final Object implicitLock; + private final PkceValidationService pkceValidationService; /** * @param tokenGranter created by @@ -132,13 +134,15 @@ public class UaaAuthorizationEndpoint extends AbstractEndpoint implements Authen final @Qualifier("openIdSessionStateCalculator") OpenIdSessionStateCalculator openIdSessionStateCalculator, final @Qualifier("authorizationRequestManager") OAuth2RequestFactory oAuth2RequestFactory, final @Qualifier("jdbcClientDetailsService") MultitenantClientServices clientDetailsService, - final @Qualifier("oauth2TokenGranter") TokenGranter tokenGranter) { + final @Qualifier("oauth2TokenGranter") TokenGranter tokenGranter, + final @Qualifier("pkceValidationServices") PkceValidationService pkceValidationService) { this.redirectResolver = redirectResolver; this.userApprovalHandler = userApprovalHandler; this.oauth2RequestValidator = oauth2RequestValidator; this.authorizationCodeServices = authorizationCodeServices; this.hybridTokenGranterForAuthCode = hybridTokenGranterForAuthCode; this.openIdSessionStateCalculator = openIdSessionStateCalculator; + this.pkceValidationService = pkceValidationService; super.setOAuth2RequestFactory(oAuth2RequestFactory); super.setClientDetailsService(clientDetailsService); @@ -184,6 +188,8 @@ public ModelAndView authorize(Map model, if (authorizationRequest.getClientId() == null) { throw new InvalidClientException("A client id must be provided"); } + + validateAuthorizationRequestPkceParameters(authorizationRequest.getRequestParameters()); String resolvedRedirect = ""; try { @@ -268,6 +274,32 @@ public ModelAndView authorize(Map model, } } + + /** + * PKCE parameters check: + * code_challenge: (Optional) Must be provided for PKCE and must not be empty. + * code_challenge_method: (Optional) Default value is "plain". See .well-known + * endpoint for supported code challenge methods list. + * @param authorizationRequestParameters Authorization request parameters. + */ + protected void validateAuthorizationRequestPkceParameters(Map authorizationRequestParameters) { + if (pkceValidationService != null) { + String codeChallenge = authorizationRequestParameters.get(PkceValidationService.CODE_CHALLENGE); + if (codeChallenge != null) { + if(!PkceValidationService.isCodeChallengeParameterValid(codeChallenge)) { + throw new InvalidRequestException("Code challenge length must between 43 and 128 and use only [A-Z],[a-z],[0-9],_,.,-,~ characters."); + } + String codeChallengeMethod = authorizationRequestParameters.get(PkceValidationService.CODE_CHALLENGE_METHOD); + if (codeChallengeMethod == null) { + codeChallengeMethod = "plain"; + } + if (!pkceValidationService.isCodeChallengeMethodSupported(codeChallengeMethod)) { + throw new InvalidRequestException("Unsupported code challenge method. Supported: " + + pkceValidationService.getSupportedCodeChallengeMethods().toString()); + } + } + } + } // This method handles /oauth/authorize calls when user is not logged in and the prompt=none param is used @Override diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/UaaTokenServices.java b/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/UaaTokenServices.java index 832c2574b23..4c570784287 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/UaaTokenServices.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/UaaTokenServices.java @@ -27,13 +27,16 @@ import org.cloudfoundry.identity.uaa.oauth.refresh.CompositeExpiringOAuth2RefreshToken; import org.cloudfoundry.identity.uaa.oauth.refresh.RefreshTokenCreator; import org.cloudfoundry.identity.uaa.oauth.refresh.RefreshTokenRequestData; +import org.cloudfoundry.identity.uaa.oauth.token.Claims; import org.cloudfoundry.identity.uaa.oauth.token.CompositeToken; +import org.cloudfoundry.identity.uaa.oauth.token.JdbcRevocableTokenProvisioning; import org.cloudfoundry.identity.uaa.oauth.token.RevocableToken; import org.cloudfoundry.identity.uaa.oauth.token.RevocableTokenProvisioning; import org.cloudfoundry.identity.uaa.provider.oauth.ExternalOAuthUserAuthority; import org.cloudfoundry.identity.uaa.user.UaaAuthority; import org.cloudfoundry.identity.uaa.user.UaaUser; import org.cloudfoundry.identity.uaa.user.UaaUserDatabase; +import org.cloudfoundry.identity.uaa.user.UaaUserPrototype; import org.cloudfoundry.identity.uaa.user.UserInfo; import org.cloudfoundry.identity.uaa.util.JsonUtils; import org.cloudfoundry.identity.uaa.util.TimeService; @@ -102,7 +105,7 @@ import static org.cloudfoundry.identity.uaa.oauth.token.ClaimConstants.CID; import static org.cloudfoundry.identity.uaa.oauth.token.ClaimConstants.CLIENT_ID; import static org.cloudfoundry.identity.uaa.oauth.token.ClaimConstants.EMAIL; -import static org.cloudfoundry.identity.uaa.oauth.token.ClaimConstants.EXP; +import static org.cloudfoundry.identity.uaa.oauth.token.ClaimConstants.EXPIRY_IN_SECONDS; import static org.cloudfoundry.identity.uaa.oauth.token.ClaimConstants.GRANTED_SCOPES; import static org.cloudfoundry.identity.uaa.oauth.token.ClaimConstants.GRANT_TYPE; import static org.cloudfoundry.identity.uaa.oauth.token.ClaimConstants.IAT; @@ -142,7 +145,7 @@ public class UaaTokenServices implements AuthorizationServerTokenServices, Resou CLIENT_ID, CID, AZP, REVOCABLE, GRANT_TYPE, USER_ID, ORIGIN, USER_NAME, EMAIL, AUTH_TIME, REVOCATION_SIGNATURE, IAT, - EXP, ISS, ZONE_ID, AUD + EXPIRY_IN_SECONDS, ISS, ZONE_ID, AUD ); private final Logger logger = LoggerFactory.getLogger(UaaTokenServices.class); private UaaUserDatabase userDatabase; @@ -229,20 +232,27 @@ public OAuth2AccessToken refreshAccessToken(String refreshTokenValue, TokenReque Map refreshTokenClaims = tokenValidation.getClaims(); ArrayList tokenScopes = getScopesFromRefreshToken(refreshTokenClaims); - refreshTokenCreator.ensureRefreshTokenCreationNotRestricted(tokenScopes); - String userId = (String) refreshTokenClaims.get(USER_ID); - String refreshTokenId = (String) refreshTokenClaims.get(JTI); - Integer refreshTokenExpirySeconds = (Integer) refreshTokenClaims.get(EXP); - String clientId = (String) refreshTokenClaims.get(CID); - Boolean revocableClaim = (Boolean) refreshTokenClaims.get(REVOCABLE); - String refreshGrantType = refreshTokenClaims.get(GRANT_TYPE).toString(); - String nonce = (String) refreshTokenClaims.get(NONCE); - String revocableHashSignature = (String) refreshTokenClaims.get(REVOCATION_SIGNATURE); - Map additionalAuthorizationInfo = (Map) refreshTokenClaims.get(ADDITIONAL_AZ_ATTR); - Set audience = new HashSet<>((ArrayList) refreshTokenClaims.get(AUD)); - Integer authTime = (Integer) refreshTokenClaims.get(AUTH_TIME); + Claims claims; + try { + String s = JsonUtils.writeValueAsString(refreshTokenClaims); + claims = JsonUtils.readValue(s, Claims.class); + } catch (JsonUtils.JsonUtilException e) { + logger.error("Cannot read token claims", e); + throw new InvalidTokenException("Cannot read token claims", e); + } + String userId = claims.getUserId(); + String refreshTokenId = claims.getJti(); + Long refreshTokenExpirySeconds = claims.getExp(); + String clientId = claims.getCid(); + Boolean revocableClaim = claims.isRevocable(); + String refreshGrantType = claims.getGrantType(); + String nonce = claims.getNonce(); + String revocableHashSignature = claims.getRevSig(); + Map additionalAuthorizationInfo = claims.getAzAttr(); + Set audience = Set.copyOf(claims.getAud()); + Long authTime = claims.getAuthTime(); // default request scopes to what is in the refresh token Set requestedScopes = request.getScope().isEmpty() ? Sets.newHashSet(tokenScopes) : request.getScope(); @@ -257,7 +267,7 @@ public OAuth2AccessToken refreshAccessToken(String refreshTokenValue, TokenReque boolean isRevocable = isOpaque || (revocableClaim == null ? false : revocableClaim); - UaaUser user = userDatabase.retrieveUserById(userId); + UaaUser user = new UaaUser(userDatabase.retrieveUserPrototypeById(userId)); BaseClientDetails client = (BaseClientDetails) clientDetailsService.loadClientByClientId(clientId); long refreshTokenExpireMillis = refreshTokenExpirySeconds.longValue() * 1000L; @@ -522,7 +532,7 @@ private KeyInfo getActiveKeyInfo() { } claims.put(IAT, timeService.getCurrentTimeMillis() / 1000); - claims.put(EXP, token.getExpiration().getTime() / 1000); + claims.put(EXPIRY_IN_SECONDS, token.getExpiration().getTime() / 1000); if (tokenEndpointBuilder.getTokenEndpoint(IdentityZoneHolder.get()) != null) { claims.put(ISS, tokenEndpointBuilder.getTokenEndpoint(IdentityZoneHolder.get())); @@ -692,11 +702,7 @@ CompositeToken persistRevocableToken(String tokenId, .setUserId(userId) .setScope(scope) .setValue(token.getValue()); - try { - tokenProvisioning.create(revocableAccessToken, IdentityZoneHolder.get().getId()); - } catch (DuplicateKeyException updateInstead) { - tokenProvisioning.update(tokenId, revocableAccessToken, IdentityZoneHolder.get().getId()); - } + tokenProvisioning.upsert(tokenId, revocableAccessToken, IdentityZoneHolder.get().getId()); } boolean isRefreshTokenOpaque = isOpaque || OPAQUE.getStringValue().equals(getActiveTokenPolicy().getRefreshTokenFormat()); @@ -714,14 +720,10 @@ CompositeToken persistRevocableToken(String tokenId, .setUserId(userId) .setScope(scope) .setValue(refreshToken.getValue()); - try { - if(refreshTokenUnique) { - tokenProvisioning.deleteRefreshTokensForClientAndUserId(clientId, userId, IdentityZoneHolder.get().getId()); - } - tokenProvisioning.create(revocableRefreshToken, IdentityZoneHolder.get().getId()); - } catch (DuplicateKeyException ignore) { - //no need to store refresh tokens again + if(refreshTokenUnique) { + tokenProvisioning.deleteRefreshTokensForClientAndUserId(clientId, userId, IdentityZoneHolder.get().getId()); } + tokenProvisioning.createIfNotExists(revocableRefreshToken, IdentityZoneHolder.get().getId()); } CompositeToken result = new CompositeToken(isOpaque ? tokenId : token.getValue()); @@ -779,7 +781,7 @@ public OAuth2Authentication loadAuthentication(String accessToken) throws Authen accessToken = tokenValidation.getJwt().getEncoded(); // Check token expiry - Long expiration = Long.valueOf(claims.get(EXP).toString()); + Long expiration = Long.valueOf(claims.get(EXPIRY_IN_SECONDS).toString()); if (new Date(expiration * 1000L).before(timeService.getCurrentDate())) { throw new InvalidTokenException("Invalid access token: expired at " + new Date(expiration * 1000L)); } @@ -815,7 +817,7 @@ public OAuth2Authentication loadAuthentication(String accessToken) throws Authen Authentication userAuthentication = null; // Is this a user token - minimum info is user_id if (claims.containsKey(USER_ID)) { - UaaUser user = userDatabase.retrieveUserById((String)claims.get(USER_ID)); + UaaUserPrototype user = userDatabase.retrieveUserPrototypeById((String)claims.get(USER_ID)); UaaPrincipal principal = new UaaPrincipal(user); userAuthentication = new UaaAuthentication(principal, UaaAuthority.USER_AUTHORITIES, null); } else { @@ -852,7 +854,7 @@ public OAuth2AccessToken readAccessToken(String accessToken) { // Expiry is verified by check_token CompositeToken token = new CompositeToken(accessToken); token.setTokenType(OAuth2AccessToken.BEARER_TYPE); - token.setExpiration(new Date(Long.valueOf(claims.get(EXP).toString()) * 1000L)); + token.setExpiration(new Date(Long.valueOf(claims.get(EXPIRY_IN_SECONDS).toString()) * 1000L)); @SuppressWarnings("unchecked") ArrayList scopes = (ArrayList) claims.get(SCOPE); diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/openid/IdToken.java b/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/openid/IdToken.java index 2dea49a0099..8ff3aec7916 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/openid/IdToken.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/openid/IdToken.java @@ -17,7 +17,7 @@ import static org.cloudfoundry.identity.uaa.oauth.token.ClaimConstants.CID; import static org.cloudfoundry.identity.uaa.oauth.token.ClaimConstants.CLIENT_ID; import static org.cloudfoundry.identity.uaa.oauth.token.ClaimConstants.EMAIL_VERIFIED; -import static org.cloudfoundry.identity.uaa.oauth.token.ClaimConstants.EXP; +import static org.cloudfoundry.identity.uaa.oauth.token.ClaimConstants.EXPIRY_IN_SECONDS; import static org.cloudfoundry.identity.uaa.oauth.token.ClaimConstants.FAMILY_NAME; import static org.cloudfoundry.identity.uaa.oauth.token.ClaimConstants.GIVEN_NAME; import static org.cloudfoundry.identity.uaa.oauth.token.ClaimConstants.GRANT_TYPE; @@ -137,7 +137,7 @@ public String getClientId() { return clientId; } - @JsonProperty(EXP) + @JsonProperty(EXPIRY_IN_SECONDS) public Long getExpInSeconds() { return exp.getTime() / 1000; } diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/openid/IdTokenCreator.java b/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/openid/IdTokenCreator.java index b9b0e030136..37eb596cba1 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/openid/IdTokenCreator.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/openid/IdTokenCreator.java @@ -11,7 +11,6 @@ import org.cloudfoundry.identity.uaa.user.UaaUserDatabase; import org.cloudfoundry.identity.uaa.util.TimeService; import org.cloudfoundry.identity.uaa.zone.MultitenantClientServices; -import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.oauth2.provider.ClientDetails; import java.util.Date; @@ -28,7 +27,7 @@ import static org.cloudfoundry.identity.uaa.oauth.token.ClaimConstants.CID; import static org.cloudfoundry.identity.uaa.oauth.token.ClaimConstants.EMAIL; import static org.cloudfoundry.identity.uaa.oauth.token.ClaimConstants.EMAIL_VERIFIED; -import static org.cloudfoundry.identity.uaa.oauth.token.ClaimConstants.EXP; +import static org.cloudfoundry.identity.uaa.oauth.token.ClaimConstants.EXPIRY_IN_SECONDS; import static org.cloudfoundry.identity.uaa.oauth.token.ClaimConstants.FAMILY_NAME; import static org.cloudfoundry.identity.uaa.oauth.token.ClaimConstants.GIVEN_NAME; import static org.cloudfoundry.identity.uaa.oauth.token.ClaimConstants.GRANT_TYPE; @@ -95,7 +94,7 @@ public IdToken create(ClientDetails clientDetails, getIfNotExcluded(uaaUser.getId(), USER_ID), getIfNotExcluded(newArrayList(clientDetails.getClientId()), AUD), getIfNotExcluded(issuerUrl, ISS), - getIfNotExcluded(expiryDate, EXP), + getIfNotExcluded(expiryDate, EXPIRY_IN_SECONDS), getIfNotExcluded(issuedAt, IAT), getIfNotExcluded(userAuthenticationData.authTime, AUTH_TIME), getIfNotExcluded(userAuthenticationData.authenticationMethods, AMR), diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/pkce/PkceValidationException.java b/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/pkce/PkceValidationException.java new file mode 100644 index 00000000000..1064f638ff5 --- /dev/null +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/pkce/PkceValidationException.java @@ -0,0 +1,17 @@ +package org.cloudfoundry.identity.uaa.oauth.pkce; + +/** + * Universal PKCE Validation Service exception + * + * @author Zoltan Maradics + * + */ +public class PkceValidationException extends Exception{ + + private static final long serialVersionUID = 7887667018613362856L; + + public PkceValidationException(String msg) { + super(msg); + } + +} diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/pkce/PkceValidationService.java b/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/pkce/PkceValidationService.java new file mode 100644 index 00000000000..bb25fede0af --- /dev/null +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/pkce/PkceValidationService.java @@ -0,0 +1,166 @@ +package org.cloudfoundry.identity.uaa.oauth.pkce; + +import java.util.Collections; +import java.util.Map; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * PKCE Validation Service. + * - Validate Code Verifier parameter. + * - Validate Code Challenge parameter. + * - Validate Code Challenge Method parameter. + * - List supported code challenge methods. + * - Verify code verifier and code challenge based on code challenge method. + * + * @author Zoltan Maradics + */ + +public class PkceValidationService { + + /* + * Regular expression match with any string: + * - Length between 43 and 128 + * - Contains only [A-Z],[a-z],[0-9],_,.,-,~ characters + * (Note: '_' is part of the 'w' in the pattern.) + */ + private static final Pattern pattern = Pattern.compile("^[\\w\\.\\-\\~]{43,128}$"); + + public static final String CODE_CHALLENGE = "code_challenge"; + public static final String CODE_CHALLENGE_METHOD = "code_challenge_method"; + public static final String CODE_VERIFIER = "code_verifier"; + + private Map pkceVerifiers; + + public PkceValidationService() { + this(Collections.emptyMap()); + } + + public PkceValidationService(Map pkceVerifiers) { + this.pkceVerifiers = pkceVerifiers; + } + + /** + * Get all supported code challenge methods. + * @return Set of supported code challenge methods. + */ + public Set getSupportedCodeChallengeMethods() { + Set supportedCodeChallengeMethods = this.pkceVerifiers.keySet(); + return supportedCodeChallengeMethods; + } + + /** + * Check code challenge method is supported or not. + * @param codeChallengeMethod + * Code challenge method parameter. + * @return true if the code challenge method is supported. + * false otherwise. + */ + public boolean isCodeChallengeMethodSupported(String codeChallengeMethod) { + if (codeChallengeMethod == null) { + return false; + } + return this.pkceVerifiers.containsKey(codeChallengeMethod); + } + + /** + * Check presence of PKCE parameters and validate. + * @param requestParameters + * Map of query parameters of Authorization request. + * @param codeVerifier + * Code verifier. + * @return true: (1) in case of Authorization Code Grant without PKCE. + * (2) in case of Authorization Code Grant with PKCE and code verifier + * matched with code challenge based on code challenge method. + * false: in case of Authorization Code Grant with PKCE and code verifier + * does not match with code challenge based on code challenge method. + * @throws PkceValidationException + * (1) Code verifier must be provided for this authorization code. + * (2) Code verifier not required for this authorization code. + */ + public boolean checkAndValidate(Map requestParameters, String codeVerifier) throws PkceValidationException { + if (!hasPkceParameters(requestParameters, codeVerifier)) { + return true; + } + String codeChallengeMethod = extractCodeChallengeMethod(requestParameters); + return pkceVerifiers.get(codeChallengeMethod).verify(codeVerifier, + requestParameters.get(PkceValidationService.CODE_CHALLENGE)); + } + + /** + * Check if PKCE parameters are present. + * @param requestParameters + * Map of authorization request parameters. + * @param codeVerifier + * Code verifier. + * @return true: There are Code Challenge and Code Verifier parameters with not null value. + * false: There are no PKCE parameters. + * @throws PkceValidationException + * (1) Code verifier must be provided for this authorization code. + * (2) Code verifier not required for this authorization code. + */ + protected boolean hasPkceParameters(Map requestParameters, String codeVerifier) throws PkceValidationException{ + String codeChallenge = requestParameters.get(CODE_CHALLENGE); + if (codeChallenge != null) { + if (codeVerifier != null && !codeVerifier.isEmpty()) { + return true; + }else { + throw new PkceValidationException("Code verifier must be provided for this authorization code."); + } + }else if (codeVerifier != null && !codeVerifier.isEmpty()){ + throw new PkceValidationException("Code verifier not required for this authorization code."); + } + return false; + } + + /** + * Extract code challenge method from request. + * @param requestParameters: Authorization request parameters. + * @return + * If there is no code challenge method in authorization request then return: "plain" + * Otherwise return the value of code challenge method parameter. + */ + protected String extractCodeChallengeMethod(Map requestParameters) { + String codeChallengeMethod = requestParameters.get(CODE_CHALLENGE_METHOD); + if (codeChallengeMethod == null) { + return "plain"; + }else { + return codeChallengeMethod; + } + } + + /** + * Validate the code verifier parameter based on RFC 7636 recommendations. + * + * @param codeVerifier: Code Verifier parameter from token request. + * @return true or false based on evaluation. + */ + public static boolean isCodeVerifierParameterValid(String codeVerifier) { + return matchWithPattern(codeVerifier); + } + + /** + * Validate the code challenge parameter based on RFC 7636 recommendations. + * + * @param codeChallenge: Code Challenge parameter from token request. + * @return true or false based on evaluation. + */ + public static boolean isCodeChallengeParameterValid(String codeChallenge) { + return matchWithPattern(codeChallenge); + } + + /** + * Validate parameter with predefined regular expression (length and used + * character set) + * + * @param parameter: Code Verifier or Code Challenge + * @return true or false based on parameter match with regular expression + */ + protected static boolean matchWithPattern(String parameter) { + if (parameter == null) { + return false; + } + return pattern.matcher(parameter).matches(); + } +} diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/pkce/PkceVerifier.java b/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/pkce/PkceVerifier.java new file mode 100644 index 00000000000..9977d6db46a --- /dev/null +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/pkce/PkceVerifier.java @@ -0,0 +1,32 @@ +package org.cloudfoundry.identity.uaa.oauth.pkce; + +/** + * Each PKCE verifier MUST implement this interface to be able to be used + * in PKCE validation service. + * + * @author Zoltan Maradics + * + */ +public interface PkceVerifier { + + /** + * Verify that the code verifier matches the code challenge based on code challenge method. + * code_challenge = code_challenge_method(code_verifier) + * + * @param codeVerifier + * Code verifier parameter. + * @param codeChallenge + * Code challenge parameter. + * @return true: if code verifier transformed with code challenge method match + * with code challenge. + * false: otherwise. + */ + public boolean verify(String codeVerifier, String codeChallenge); + + /** + * Getter for Code Challenge Method name. + * + * @return + */ + public String getCodeChallengeMethod(); +} diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/pkce/verifiers/PlainPkceVerifier.java b/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/pkce/verifiers/PlainPkceVerifier.java new file mode 100644 index 00000000000..d2e89da6f75 --- /dev/null +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/pkce/verifiers/PlainPkceVerifier.java @@ -0,0 +1,27 @@ +package org.cloudfoundry.identity.uaa.oauth.pkce.verifiers; + +import org.cloudfoundry.identity.uaa.oauth.pkce.PkceVerifier; + +/** + * Plain code challenge method implementation. + * + * @author Zoltan Maradics + * + */ +public class PlainPkceVerifier implements PkceVerifier{ + + private final String codeChallengeMethod = "plain"; + + @Override + public boolean verify(String codeVerifier, String codeChallenge) { + if (codeVerifier == null || codeChallenge == null) { + return false; + } + return codeChallenge.equals(codeVerifier); + } + + @Override + public String getCodeChallengeMethod() { + return codeChallengeMethod; + } +} diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/pkce/verifiers/S256PkceVerifier.java b/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/pkce/verifiers/S256PkceVerifier.java new file mode 100644 index 00000000000..1b00b819d2c --- /dev/null +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/pkce/verifiers/S256PkceVerifier.java @@ -0,0 +1,53 @@ +package org.cloudfoundry.identity.uaa.oauth.pkce.verifiers; + +import java.io.UnsupportedEncodingException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +import org.apache.commons.codec.binary.Base64; +import org.cloudfoundry.identity.uaa.oauth.pkce.PkceVerifier; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * SHA-256 code challenge method implementation. + * + * @author Zoltan Maradics + * + */ +public class S256PkceVerifier implements PkceVerifier { + + private static Logger logger = LoggerFactory.getLogger(S256PkceVerifier.class); + private final String codeChallengeMethod = "S256"; + + public S256PkceVerifier() { + } + + @Override + public boolean verify(String codeVerifier, String codeChallenge) { + if (codeVerifier == null || codeChallenge == null) { + return false; + } + return codeChallenge.contentEquals(compute(codeVerifier)); + } + + public String compute(String codeVerifier) { + try { + byte[] bytes = codeVerifier.getBytes("US-ASCII"); + MessageDigest md = MessageDigest.getInstance("SHA-256"); + md.update(bytes, 0, bytes.length); + byte[] digest = md.digest(); + return Base64.encodeBase64URLSafeString(digest); + } catch (UnsupportedEncodingException e) { + logger.debug(e.getMessage(),e); + } catch (NoSuchAlgorithmException e) { + logger.debug(e.getMessage(),e); + } + return null; + } + + @Override + public String getCodeChallengeMethod() { + return codeChallengeMethod; + } +} diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/refresh/RefreshTokenCreator.java b/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/refresh/RefreshTokenCreator.java index 87c32d1064c..fcfbb6ef0ba 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/refresh/RefreshTokenCreator.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/refresh/RefreshTokenCreator.java @@ -80,7 +80,7 @@ private String buildJwtToken(UaaUser user, claims.put(JTI, tokenId); claims.put(SUB, user.getId()); claims.put(IAT, timeService.getCurrentTimeMillis() / 1000); - claims.put(EXP, expirationDate.getTime() / 1000); + claims.put(EXPIRY_IN_SECONDS, expirationDate.getTime() / 1000); claims.put(CID, tokenRequestData.clientId); claims.put(CLIENT_ID, tokenRequestData.clientId); claims.put(ISS, tokenEndpointBuilder.getTokenEndpoint(IdentityZoneHolder.get())); diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/token/JdbcRevocableTokenProvisioning.java b/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/token/JdbcRevocableTokenProvisioning.java index c2fe03393ff..b786196fe15 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/token/JdbcRevocableTokenProvisioning.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/token/JdbcRevocableTokenProvisioning.java @@ -26,7 +26,10 @@ public class JdbcRevocableTokenProvisioning implements RevocableTokenProvisionin private final static String FIELDS = "token_id,client_id,user_id,format,response_type,issued_at,expires_at,scope,data,identity_zone_id"; private final static String UPDATE_FIELDS = FIELDS.substring(FIELDS.indexOf(',') + 1, FIELDS.lastIndexOf(',')).replace(",", "=?,") + "=?"; private final static String TABLE = "revocable_tokens"; + private static final String SELECT = "SELECT "; + private static final String FROM = " FROM "; private final static String GET_QUERY = "SELECT " + FIELDS + " FROM " + TABLE + " WHERE token_id=? AND identity_zone_id=?"; + private final static String GET_COUNT_QUERY = "SELECT COUNT(*) FROM " + TABLE + " WHERE token_id=? AND identity_zone_id=?"; private final static String GET_BY_USER_QUERY = "SELECT " + FIELDS + " FROM " + TABLE + " WHERE user_id=? AND identity_zone_id=?"; private final static String GET_BY_CLIENT_QUERY = "SELECT " + FIELDS + " FROM " + TABLE + " WHERE client_id=? AND identity_zone_id=?"; private final static String UPDATE_QUERY = "UPDATE " + TABLE + " SET " + UPDATE_FIELDS + " WHERE token_id=? and identity_zone_id=?"; @@ -61,6 +64,14 @@ public List retrieveAll(String zoneId) { return null; } + private boolean exists(String id, boolean checkExpired, String zoneId) { + if (checkExpired) { + checkExpired(); + } + Integer idResults = template.queryForObject(GET_COUNT_QUERY, Integer.class, id, zoneId); + return idResults != null && idResults == 1; + } + public RevocableToken retrieve(String id, boolean checkExpired, String zoneId) { if (checkExpired) { checkExpired(); @@ -83,6 +94,23 @@ public int deleteRefreshTokensForClientAndUserId(String clientId, String userId, return template.update(DELETE_REFRESH_TOKEN_QUERY, userId, clientId, zoneId); } + public void createIfNotExists(RevocableToken t, String zoneId) { + if (exists(t.getTokenId(), true, zoneId)) { + return; + } + template.update(INSERT_QUERY, + t.getTokenId(), + t.getClientId(), + t.getUserId(), + t.getFormat(), + t.getResponseType().toString(), + t.getIssuedAt(), + t.getExpiresAt(), + t.getScope(), + t.getValue(), + zoneId); + } + @Override public RevocableToken create(RevocableToken t, String zoneId) { checkExpired(); @@ -116,6 +144,34 @@ public RevocableToken update(String id, RevocableToken t, String zoneId) { return retrieve(id, false, zoneId); } + public void upsert(String id, RevocableToken t, String zoneId) { + if (exists(t.getTokenId(), true, zoneId)) { + template.update(UPDATE_QUERY, // NOSONAR + t.getClientId(), + t.getUserId(), + t.getFormat(), + t.getResponseType().toString(), + t.getIssuedAt(), + t.getExpiresAt(), + t.getScope(), + t.getValue(), + id, + zoneId); + } else { + template.update(INSERT_QUERY, + t.getTokenId(), + t.getClientId(), + t.getUserId(), + t.getFormat(), + t.getResponseType().toString(), + t.getIssuedAt(), + t.getExpiresAt(), + t.getScope(), + t.getValue(), + zoneId); + } + } + @Override public RevocableToken delete(String id, int version, String zoneId) { RevocableToken previous = retrieve(id, false, zoneId); diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/token/PkceEnhancedAuthorizationCodeTokenGranter.java b/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/token/PkceEnhancedAuthorizationCodeTokenGranter.java new file mode 100644 index 00000000000..a8bbd878ee7 --- /dev/null +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/token/PkceEnhancedAuthorizationCodeTokenGranter.java @@ -0,0 +1,121 @@ +package org.cloudfoundry.identity.uaa.oauth.token; + +import java.util.HashMap; +import java.util.Map; + +import org.cloudfoundry.identity.uaa.oauth.pkce.PkceValidationException; +import org.cloudfoundry.identity.uaa.oauth.pkce.PkceValidationService; +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.common.exceptions.InvalidClientException; +import org.springframework.security.oauth2.common.exceptions.InvalidGrantException; +import org.springframework.security.oauth2.common.exceptions.InvalidRequestException; +import org.springframework.security.oauth2.common.exceptions.RedirectMismatchException; +import org.springframework.security.oauth2.common.util.OAuth2Utils; +import org.springframework.security.oauth2.provider.ClientDetails; +import org.springframework.security.oauth2.provider.ClientDetailsService; +import org.springframework.security.oauth2.provider.OAuth2Authentication; +import org.springframework.security.oauth2.provider.OAuth2Request; +import org.springframework.security.oauth2.provider.OAuth2RequestFactory; +import org.springframework.security.oauth2.provider.TokenRequest; +import org.springframework.security.oauth2.provider.code.AuthorizationCodeServices; +import org.springframework.security.oauth2.provider.code.AuthorizationCodeTokenGranter; +import org.springframework.security.oauth2.provider.token.AuthorizationServerTokenServices; + +public class PkceEnhancedAuthorizationCodeTokenGranter extends AuthorizationCodeTokenGranter { + + private final AuthorizationCodeServices authorizationCodeServices; + + private PkceValidationService pkceValidationService; + + public PkceEnhancedAuthorizationCodeTokenGranter(AuthorizationServerTokenServices tokenServices, + AuthorizationCodeServices authorizationCodeServices, ClientDetailsService clientDetailsService, + OAuth2RequestFactory requestFactory) { + super(tokenServices, authorizationCodeServices, clientDetailsService, requestFactory); + this.authorizationCodeServices = authorizationCodeServices; + } + + @Override + protected OAuth2Authentication getOAuth2Authentication(ClientDetails client, TokenRequest tokenRequest) { + + Map parameters = tokenRequest.getRequestParameters(); + String authorizationCode = parameters.get("code"); + String redirectUri = parameters.get(OAuth2Utils.REDIRECT_URI); + + if (authorizationCode == null) { + throw new InvalidRequestException("An authorization code must be supplied."); + } + + /* + * PKCE code verifier parameter length and charset validation + */ + String codeVerifier = parameters.get(PkceValidationService.CODE_VERIFIER); + if (codeVerifier != null && !PkceValidationService.isCodeVerifierParameterValid(codeVerifier)) { + throw new InvalidRequestException("Code verifier length must between 43 and 128 and use only [A-Z],[a-z],[0-9],_,.,-,~ characters."); + } + + OAuth2Authentication storedAuth = authorizationCodeServices.consumeAuthorizationCode(authorizationCode); + if (storedAuth == null) { + throw new InvalidGrantException("Invalid authorization code: " + authorizationCode); + } + + /* + * PKCE code verifier parameter verification + */ + try { + if (pkceValidationService != null && !pkceValidationService.checkAndValidate(storedAuth.getOAuth2Request().getRequestParameters(), codeVerifier)) { + // has PkceValidation service and validation failed + throw new InvalidGrantException("Invalid code verifier: " + codeVerifier); + } + } catch (PkceValidationException exception) { + // during the validation one of the PKCE parameters missing + throw new InvalidGrantException("PKCE error: "+ exception.getMessage()); + } + // No pkceValidationService defined or Pkce validation successfully passed + + OAuth2Request pendingOAuth2Request = storedAuth.getOAuth2Request(); + // https://jira.springsource.org/browse/SECOAUTH-333 + // This might be null, if the authorization was done without the redirect_uri + // parameter + String redirectUriApprovalParameter = pendingOAuth2Request.getRequestParameters().get(OAuth2Utils.REDIRECT_URI); + + if ((redirectUri != null || redirectUriApprovalParameter != null) + && !pendingOAuth2Request.getRedirectUri().equals(redirectUri)) { + throw new RedirectMismatchException("Redirect URI mismatch."); + } + + String pendingClientId = pendingOAuth2Request.getClientId(); + String clientId = tokenRequest.getClientId(); + if (clientId != null && !clientId.equals(pendingClientId)) { + // just a sanity check. + throw new InvalidClientException("Client ID mismatch"); + } + + // Secret is not required in the authorization request, so it won't be available + // in the pendingAuthorizationRequest. We do want to check that a secret is + // provided + // in the token request, but that happens elsewhere. + + Map combinedParameters = new HashMap( + pendingOAuth2Request.getRequestParameters()); + // Combine the parameters adding the new ones last so they override if there are + // any clashes + combinedParameters.putAll(parameters); + + // Make a new stored request with the combined parameters + OAuth2Request finalStoredOAuth2Request = pendingOAuth2Request.createOAuth2Request(combinedParameters); + + Authentication userAuth = storedAuth.getUserAuthentication(); + + return new OAuth2Authentication(finalStoredOAuth2Request, userAuth); + + } + + public PkceValidationService getPkceValidationService() { + return pkceValidationService; + } + + public void setPkceValidationService(PkceValidationService pkceValidationService) { + this.pkceValidationService = pkceValidationService; + } + +} diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/token/RevocableTokenProvisioning.java b/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/token/RevocableTokenProvisioning.java index 982af70f3a7..e88f0641bcd 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/token/RevocableTokenProvisioning.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/oauth/token/RevocableTokenProvisioning.java @@ -26,6 +26,7 @@ public interface RevocableTokenProvisioning extends ResourceManager getClientTokens(String clientId, String zoneId); + void upsert(String id, RevocableToken t, String zoneId); - + void createIfNotExists(RevocableToken t, String zoneId); } diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderEndpoints.java b/server/src/main/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderEndpoints.java index 66adefdad0d..804dafb2e8c 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderEndpoints.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderEndpoints.java @@ -127,7 +127,7 @@ public ResponseEntity createIdentityProvider(@RequestBody Iden } catch (IdpAlreadyExistsException e) { return new ResponseEntity<>(body, CONFLICT); } catch (Exception x) { - logger.debug("Unable to create IdentityProvider[origin="+body.getOriginKey()+"; zone="+body.getIdentityZoneId()+"]", x); + logger.error("Unable to create IdentityProvider[origin="+body.getOriginKey()+"; zone="+body.getIdentityZoneId()+"]", x); return new ResponseEntity<>(body, INTERNAL_SERVER_ERROR); } } @@ -139,6 +139,7 @@ public ResponseEntity deleteIdentityProvider(@PathVariable Str if (publisher!=null && existing!=null) { existing.setSerializeConfigRaw(rawConfig); publisher.publishEvent(new EntityDeletedEvent<>(existing, SecurityContextHolder.getContext().getAuthentication(), identityZoneManager.getCurrentIdentityZoneId())); + redactSensitiveData(existing); return new ResponseEntity<>(existing, OK); } else { return new ResponseEntity<>(UNPROCESSABLE_ENTITY); @@ -240,7 +241,7 @@ public ResponseEntity testIdentityProvider(@RequestBody IdentityProvider status = BAD_REQUEST; exception = getExceptionString(x); } catch (Exception x) { - logger.debug("Identity provider validation failed.", x); + logger.error("Identity provider validation failed.", x); status = INTERNAL_SERVER_ERROR; exception = "check server logs"; }finally { diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/provider/oauth/ExternalOAuthAuthenticationManager.java b/server/src/main/java/org/cloudfoundry/identity/uaa/provider/oauth/ExternalOAuthAuthenticationManager.java index 9858e35a081..41922a35ffa 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/provider/oauth/ExternalOAuthAuthenticationManager.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/provider/oauth/ExternalOAuthAuthenticationManager.java @@ -37,6 +37,7 @@ import org.cloudfoundry.identity.uaa.user.UaaUserPrototype; import org.cloudfoundry.identity.uaa.util.JsonUtils; import org.cloudfoundry.identity.uaa.util.LinkedMaskingMultiValueMap; +import org.cloudfoundry.identity.uaa.util.SessionUtils; import org.cloudfoundry.identity.uaa.util.TokenValidation; import org.cloudfoundry.identity.uaa.zone.IdentityZoneHolder; import org.slf4j.Logger; @@ -63,6 +64,7 @@ import org.springframework.web.client.RestTemplate; import org.springframework.web.context.request.RequestAttributes; import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; import java.net.URI; import java.net.URISyntaxException; @@ -96,6 +98,7 @@ import static org.cloudfoundry.identity.uaa.provider.ExternalIdentityProviderDefinition.USER_NAME_ATTRIBUTE_NAME; import static org.cloudfoundry.identity.uaa.util.TokenValidation.buildIdTokenValidator; import static org.cloudfoundry.identity.uaa.util.UaaHttpRequestUtils.isAcceptedInvitationAuthentication; +import static org.springframework.http.HttpMethod.GET; import static org.springframework.util.StringUtils.hasText; import static org.springframework.util.StringUtils.isEmpty; @@ -140,15 +143,14 @@ public void setOrigin(String origin) { public IdentityProvider resolveOriginProvider(String idToken) throws AuthenticationException { try { - String claimsString = JwtHelper.decode(ofNullable(idToken).orElse("")).getClaims(); - Map claims = JsonUtils.readValue(claimsString, new TypeReference>() {}); + Map claims = parseClaimsFromIdTokenString(idToken); String issuer = (String) claims.get(ClaimConstants.ISS); if (isEmpty(issuer)) { throw new InsufficientAuthenticationException("Issuer is missing in id_token"); } //1. Check if issuer is registered provider try { - return ((ExternalOAuthProviderConfigurator) getProviderProvisioning()).retrieveByIssuer(issuer, IdentityZoneHolder.get().getId()); + return retrieveRegisteredIdentityProviderByIssuer(issuer); } catch (IncorrectResultSizeDataAccessException x) { logger.debug("No registered identity provider found for given issuer. Checking for uaa."); } @@ -167,6 +169,15 @@ public IdentityProvider resolveOriginProvider(String idToken) throws Authenticat } } + private IdentityProvider retrieveRegisteredIdentityProviderByIssuer(String issuer) { + return ((ExternalOAuthProviderConfigurator) getProviderProvisioning()).retrieveByIssuer(issuer, IdentityZoneHolder.get().getId()); + } + + private Map parseClaimsFromIdTokenString(String idToken) { + String claimsString = JwtHelper.decode(ofNullable(idToken).orElse("")).getClaims(); + return JsonUtils.readValue(claimsString, new TypeReference>() {}); + } + private boolean idTokenWasIssuedByTheUaa(String issuer) { return issuer.equals(tokenEndpointBuilder.getTokenEndpoint(IdentityZoneHolder.get())); } @@ -174,6 +185,8 @@ private boolean idTokenWasIssuedByTheUaa(String issuer) { private IdentityProvider buildInternalUaaIdpConfig(String issuer, String originKey) { OIDCIdentityProviderDefinition uaaOidcProviderConfig = new OIDCIdentityProviderDefinition(); uaaOidcProviderConfig.setIssuer(issuer); + Map userNameMapping = Collections.singletonMap(USER_NAME_ATTRIBUTE_NAME, USER_NAME_ATTRIBUTE_NAME); + uaaOidcProviderConfig.setAttributeMappings(userNameMapping); IdentityProvider uaaIdp = new IdentityProvider<>(); uaaIdp.setOriginKey(originKey); uaaIdp.setConfig(uaaOidcProviderConfig); @@ -393,34 +406,54 @@ protected UaaUser userAuthenticated(Authentication request, UaaUser userFromRequ if (is_invitation_acceptance) { String invitedUserId = (String) RequestContextHolder.currentRequestAttributes().getAttribute("user_id", RequestAttributes.SCOPE_SESSION); logger.debug("ExternalOAuth user accepted invitation, user_id:"+invitedUserId); - userFromDb = getUserDatabase().retrieveUserById(invitedUserId); + userFromDb = new UaaUser(getUserDatabase().retrieveUserPrototypeById(invitedUserId)); if (email != null) { if (!email.equalsIgnoreCase(userFromDb.getEmail())) { throw new BadCredentialsException("OAuth User email mismatch. Authenticated email doesn't match invited email."); } } publish(new InvitedUserAuthenticatedEvent(userFromDb)); - userFromDb = getUserDatabase().retrieveUserById(invitedUserId); + userFromDb = new UaaUser(getUserDatabase().retrieveUserPrototypeById(invitedUserId)); } //we must check and see if the email address has changed between authentications - if (haveUserAttributesChanged(userFromDb, userFromRequest)) { - logger.debug("User attributed have changed, updating them."); - userFromDb = userFromDb.modifyAttributes(email, - userFromRequest.getGivenName(), - userFromRequest.getFamilyName(), - userFromRequest.getPhoneNumber(), - userFromRequest.getExternalId(), - userFromDb.isVerified() || userFromRequest.isVerified()) - .modifyUsername(userFromRequest.getUsername()); - userModified = true; - } + if (haveUserAttributesChanged(userFromDb, userFromRequest) && isRegisteredIdpAuthentication(request)) { + logger.debug("User attributed have changed, updating them."); + userFromDb = userFromDb.modifyAttributes(email, + userFromRequest.getGivenName(), + userFromRequest.getFamilyName(), + userFromRequest.getPhoneNumber(), + userFromRequest.getExternalId(), + userFromDb.isVerified() || userFromRequest.isVerified()) + .modifyUsername(userFromRequest.getUsername()); + userModified = true; + } ExternalGroupAuthorizationEvent event = new ExternalGroupAuthorizationEvent(userFromDb, userModified, userFromRequest.getAuthorities(), true); publish(event); return getUserDatabase().retrieveUserById(userFromDb.getId()); } + private boolean isRegisteredIdpAuthentication(Authentication request) { + String idToken = ((ExternalOAuthCodeToken) request).getIdToken(); + if (idToken == null) { + return true; + } + Map claims = parseClaimsFromIdTokenString(idToken); + String issuer = (String) claims.get(ClaimConstants.ISS); + if (idTokenWasIssuedByTheUaa(issuer)) { + try { + // check if the UAA Identity Zone is registered as an external Idp of itself + retrieveRegisteredIdentityProviderByIssuer(issuer); + return true; + } catch (IncorrectResultSizeDataAccessException e) { + return false; + } + } else { + return true; + } + } + @Override protected boolean isAddNewShadowUser() { if (!super.isAddNewShadowUser()) { @@ -442,6 +475,8 @@ protected String getResponseType(AbstractExternalOAuthIdentityProviderDefinition if (RawExternalOAuthIdentityProviderDefinition.class.isAssignableFrom(config.getClass())) { if ("signed_request".equals(config.getResponseType())) return "signed_request"; + else if ("code".equals(config.getResponseType())) + return "code"; else return "token"; } else if (OIDCIdentityProviderDefinition.class.isAssignableFrom(config.getClass())) { @@ -493,6 +528,33 @@ protected Map getClaimsFromToken(String idToken, logger.error("Exception", e); return null; } + } else if ("code".equals(config.getResponseType()) + && RawExternalOAuthIdentityProviderDefinition.class.isAssignableFrom(config.getClass()) + && ((RawExternalOAuthIdentityProviderDefinition) config).getUserInfoUrl() != null) { + RawExternalOAuthIdentityProviderDefinition narrowedConfig = (RawExternalOAuthIdentityProviderDefinition) config; + + HttpHeaders headers = new HttpHeaders(); + headers.add("Authorization", "token " + idToken); + headers.add("Accept", "application/json"); + + URI requestUri; + HttpEntity requestEntity = new HttpEntity<>(headers); + try { + requestUri = narrowedConfig.getUserInfoUrl().toURI(); + } catch (URISyntaxException exc) { + logger.error("Invalid user info URI configured: <" + narrowedConfig.getUserInfoUrl() + ">", exc); + return null; + } + + logger.debug(String.format("Performing token check with url:%s", requestUri)); + ResponseEntity> responseEntity = + getRestTemplate(config) + .exchange(requestUri, GET, requestEntity, + new ParameterizedTypeReference>() { + } + ); + logger.debug(String.format("Request completed with status:%s", responseEntity.getStatusCode())); + return responseEntity.getBody(); } else { TokenValidation validation = validateToken(idToken, config); logger.debug("Decoding id_token"); @@ -566,20 +628,30 @@ private String getTokenFromCode(ExternalOAuthCodeToken codeToken, AbstractExtern } MultiValueMap body = new LinkedMaskingMultiValueMap<>("code", "client_secret"); body.add("grant_type", GRANT_TYPE_AUTHORIZATION_CODE); - body.add("response_type", getResponseType(config)); + body.add("response_type", getResponseType(config)); // not required by the Oauth 2.0 standard in this 'Access Token Request' body.add("code", codeToken.getCode()); body.add("redirect_uri", codeToken.getRedirectUrl()); + // NOTE: the "state" body parameter is optional. We also are in + // trouble here about how to obtain the correct 'httpSession' to use. +// body.add("state", SessionUtils.getStateParam(RequestContextHolder...httpSession, SessionUtils.stateParameterAttributeKeyForIdp(codeToken.getOrigin()))); logger.debug("Adding new client_id and client_secret for token exchange"); body.add("client_id", config.getRelyingPartyId()); HttpHeaders headers = new HttpHeaders(); - if(config.isClientAuthInBody()) { - body.add("client_secret", config.getRelyingPartySecret()); + // no client-secret, switch to PKCE and treat client as public, same logic is implemented in spring security + // https://docs.spring.io/spring-security/site/docs/5.3.1.RELEASE/reference/html5/#initiating-the-authorization-request + if (config.getRelyingPartySecret() == null) { + // if session is expired or other issues in retrieven code_verifier, then flow fails with 401, which is expected + body.add("code_verifier", getSessionValue(SessionUtils.codeVerifierParameterAttributeKeyForIdp(codeToken.getOrigin()))); } else { - String clientAuthHeader = getClientAuthHeader(config); - headers.add("Authorization", clientAuthHeader); + if (config.isClientAuthInBody()) { + body.add("client_secret", config.getRelyingPartySecret()); + } else { + String clientAuthHeader = getClientAuthHeader(config); + headers.add("Authorization", clientAuthHeader); + } } headers.add("Accept", "application/json"); @@ -604,7 +676,17 @@ private String getTokenFromCode(ExternalOAuthCodeToken codeToken, AbstractExtern } ); logger.debug(String.format("Request completed with status:%s", responseEntity.getStatusCode())); - return responseEntity.getBody().get(getResponseType(config)); + return responseEntity.getBody().get(getTokenFieldName(config)); + } + + private String getSessionValue(String value) { + try { + ServletRequestAttributes attr = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes(); + return (String) SessionUtils.getStateParam(attr.getRequest().getSession(false), value); + } catch (Exception e) { + logger.warn("Exception", e); + return (String)""; + } } private String getClientAuthHeader(AbstractExternalOAuthIdentityProviderDefinition config) { @@ -612,6 +694,14 @@ private String getClientAuthHeader(AbstractExternalOAuthIdentityProviderDefiniti return "Basic " + clientAuth; } + private String getTokenFieldName(AbstractExternalOAuthIdentityProviderDefinition config) { + String responseType = getResponseType(config); + if (responseType == "code" || responseType == "token") { + return "access_token"; // Oauth 2.0 + } + return responseType; + } + public void setTokenEndpointBuilder(TokenEndpointBuilder tokenEndpointBuilder) { this.tokenEndpointBuilder = tokenEndpointBuilder; } diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/provider/oauth/ExternalOAuthIdentityProviderConfigValidator.java b/server/src/main/java/org/cloudfoundry/identity/uaa/provider/oauth/ExternalOAuthIdentityProviderConfigValidator.java index 2f50b629547..facd699a1be 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/provider/oauth/ExternalOAuthIdentityProviderConfigValidator.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/provider/oauth/ExternalOAuthIdentityProviderConfigValidator.java @@ -46,10 +46,6 @@ public void validate(AbstractIdentityProviderDefinition definition) { errors.add("Relying Party Id must be the client-id for the UAA that is registered with the external IDP"); } - if (!hasText(def.getRelyingPartySecret()) && !def.getResponseType().contains("token")) { - errors.add("Relying Party Secret must be the client-secret for the UAA that is registered with the external IDP"); - } - if (def.isShowLinkText() && !hasText(def.getLinkText())) { errors.add("Link Text must be specified because showLinkText is true"); } diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/provider/oauth/ExternalOAuthProviderConfigurator.java b/server/src/main/java/org/cloudfoundry/identity/uaa/provider/oauth/ExternalOAuthProviderConfigurator.java index 6b52cac098a..b05b4c3c94b 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/provider/oauth/ExternalOAuthProviderConfigurator.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/provider/oauth/ExternalOAuthProviderConfigurator.java @@ -1,5 +1,6 @@ package org.cloudfoundry.identity.uaa.provider.oauth; +import org.cloudfoundry.identity.uaa.oauth.pkce.verifiers.S256PkceVerifier; import org.cloudfoundry.identity.uaa.provider.AbstractExternalOAuthIdentityProviderDefinition; import org.cloudfoundry.identity.uaa.provider.IdentityProvider; import org.cloudfoundry.identity.uaa.provider.IdentityProviderProvisioning; @@ -73,6 +74,17 @@ public String getIdpAuthenticationUrl( .queryParam("redirect_uri", callbackUrl) .queryParam("state", state); + // no client-secret, switch to PKCE and treat client as public, same logic is implemented in spring security + // https://docs.spring.io/spring-security/site/docs/5.3.1.RELEASE/reference/html5/#initiating-the-authorization-request + if (definition.getRelyingPartySecret() == null) { + var pkceVerifier = new S256PkceVerifier(); + var codeVerifier = generateCodeVerifier(); + var codeChallenge = pkceVerifier.compute(codeVerifier); + SessionUtils.setStateParam(request.getSession(), SessionUtils.codeVerifierParameterAttributeKeyForIdp(idpOriginKey), codeVerifier); + uriBuilder.queryParam("code_challenge", codeChallenge); + uriBuilder.queryParam("code_challenge_method", pkceVerifier.getCodeChallengeMethod()); + } + if (!CollectionUtils.isEmpty(definition.getScopes())) { uriBuilder.queryParam("scope", URLEncoder.encode(String.join(" ", definition.getScopes()), StandardCharsets.UTF_8)); } @@ -89,6 +101,10 @@ private String generateStateParam() { return uaaRandomStringUtil.getSecureRandom(10); } + private String generateCodeVerifier() { + return uaaRandomStringUtil.getSecureRandom(128); + } + private String getCallbackUrlForIdp(String idpOriginKey, String uaaBaseUrl) { return URLEncoder.encode(uaaBaseUrl + "/login/callback/" + idpOriginKey, StandardCharsets.UTF_8); } diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/provider/oauth/OauthIDPWrapperFactoryBean.java b/server/src/main/java/org/cloudfoundry/identity/uaa/provider/oauth/OauthIDPWrapperFactoryBean.java index fd1c0efd384..3fb40f29631 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/provider/oauth/OauthIDPWrapperFactoryBean.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/provider/oauth/OauthIDPWrapperFactoryBean.java @@ -125,6 +125,7 @@ protected void setCommonProperties(Map idpDefinitionMap, Abstrac idpDefinition.setAuthUrl(new URL((String) idpDefinitionMap.get("authUrl"))); idpDefinition.setTokenKeyUrl(idpDefinitionMap.get("tokenKeyUrl") == null ? null : new URL((String) idpDefinitionMap.get("tokenKeyUrl"))); idpDefinition.setTokenUrl(new URL((String) idpDefinitionMap.get("tokenUrl"))); + idpDefinition.setUserInfoUrl(idpDefinitionMap.get("userInfoUrl") == null ? null : new URL((String) idpDefinitionMap.get("userInfoUrl"))); } } catch (MalformedURLException e) { throw new IllegalArgumentException("URL is malformed.", e); diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/provider/saml/LoginSamlAuthenticationProvider.java b/server/src/main/java/org/cloudfoundry/identity/uaa/provider/saml/LoginSamlAuthenticationProvider.java index 4d99a506175..eb2913c0c3f 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/provider/saml/LoginSamlAuthenticationProvider.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/provider/saml/LoginSamlAuthenticationProvider.java @@ -16,6 +16,7 @@ import org.cloudfoundry.identity.uaa.scim.ScimGroupExternalMembershipManager; import org.cloudfoundry.identity.uaa.user.UaaUser; import org.cloudfoundry.identity.uaa.user.UaaUserDatabase; +import org.cloudfoundry.identity.uaa.user.UaaUserPrototype; import org.cloudfoundry.identity.uaa.user.UserInfo; import org.cloudfoundry.identity.uaa.util.UaaUrlUtils; import org.cloudfoundry.identity.uaa.web.UaaSavedRequestAwareAuthenticationSuccessHandler; @@ -358,10 +359,10 @@ protected UaaUser createIfMissing(UaaPrincipal samlPrincipal, boolean addNew, Co user = userDatabase.retrieveUserByName(samlPrincipal.getName(), samlPrincipal.getOrigin()); } } catch (UsernameNotFoundException e) { - UaaUser uaaUser = userDatabase.retrieveUserByEmail(userWithSamlAttributes.getEmail(), samlPrincipal.getOrigin()); + UaaUserPrototype uaaUser = userDatabase.retrieveUserPrototypeByEmail(userWithSamlAttributes.getEmail(), samlPrincipal.getOrigin()); if (uaaUser != null) { userModified = true; - user = uaaUser.modifyUsername(samlPrincipal.getName()); + user = new UaaUser(uaaUser.withUsername(samlPrincipal.getName())); } else { if (!addNew) { throw new LoginSAMLException("SAML user does not exist. " @@ -369,7 +370,7 @@ protected UaaUser createIfMissing(UaaPrincipal samlPrincipal, boolean addNew, Co } publish(new NewUserAuthenticatedEvent(userWithSamlAttributes)); try { - user = userDatabase.retrieveUserByName(samlPrincipal.getName(), samlPrincipal.getOrigin()); + user = new UaaUser(userDatabase.retrieveUserPrototypeByName(samlPrincipal.getName(), samlPrincipal.getOrigin())); } catch (UsernameNotFoundException ex) { throw new BadCredentialsException("Unable to establish shadow user for SAML user:" + samlPrincipal.getName()); } diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/scim/jdbc/JdbcScimGroupMembershipManager.java b/server/src/main/java/org/cloudfoundry/identity/uaa/scim/jdbc/JdbcScimGroupMembershipManager.java index 3cc60dcaee0..aff18398e81 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/scim/jdbc/JdbcScimGroupMembershipManager.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/scim/jdbc/JdbcScimGroupMembershipManager.java @@ -11,7 +11,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; -import org.springframework.dao.DuplicateKeyException; import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.dao.IncorrectResultSizeDataAccessException; import org.springframework.jdbc.core.JdbcTemplate; @@ -46,6 +45,8 @@ public class JdbcScimGroupMembershipManager implements ScimGroupMembershipManage private static final String GET_MEMBER_SQL = String.format("select %s from %s where member_id=? and group_id=? and identity_zone_id=?", MEMBERSHIP_FIELDS, MEMBERSHIP_TABLE); + private static final String GET_MEMBER_COUNT_SQL = String.format("select count(*) from %s where member_id=? and group_id=? and identity_zone_id=?", MEMBERSHIP_TABLE); + private static final String DELETE_MEMBER_WITH_ORIGIN_SQL = String.format("delete from %s where member_id=? and origin = ? and identity_zone_id=?", MEMBERSHIP_TABLE); private static final String DELETE_MEMBER_SQL = String.format("delete from %s where member_id=? and group_id = ? and identity_zone_id=?", MEMBERSHIP_TABLE); @@ -143,21 +144,20 @@ public ScimGroupMember addMember(final String groupId, final ScimGroupMember mem // first validate the supplied groupId, memberId validateRequest(groupId, member, zoneId); final String type = (member.getType() == null ? ScimGroupMember.Type.USER : member.getType()).toString(); - try { - logger.debug("Associating group:" + groupId + " with member:" + member); - jdbcTemplate.update(ADD_MEMBER_SQL, ps -> { - ps.setString(1, groupId); - ps.setString(2, member.getMemberId()); - ps.setString(3, type); - ps.setNull(4, Types.VARCHAR); - ps.setTimestamp(5, new Timestamp(new Date().getTime())); - ps.setString(6, member.getOrigin()); - ps.setString(7, zoneId); - }); - } catch (DuplicateKeyException e) { + if (exists(groupId, member.getMemberId(), zoneId)) { throw new MemberAlreadyExistsException(member.getMemberId() + " is already part of the group: " + groupId); } - return getMemberById(groupId, member.getMemberId(), zoneId); + logger.debug("Associating group:" + groupId + " with member:" + member); + jdbcTemplate.update(ADD_MEMBER_SQL, ps -> { + ps.setString(1, groupId); + ps.setString(2, member.getMemberId()); + ps.setString(3, type); + ps.setNull(4, Types.VARCHAR); + ps.setTimestamp(5, new Timestamp(new Date().getTime())); + ps.setString(6, member.getOrigin()); + ps.setString(7, zoneId); + }); + return getMemberById(groupId, member, ScimGroupMember.Type.valueOf(type)); } @Override @@ -263,6 +263,17 @@ public ScimGroupMember getMemberById(String groupId, String memberId, String zon } } + private ScimGroupMember getMemberById(String groupId, ScimGroupMember member, ScimGroupMember.Type type) { + ScimGroupMember sgm = new ScimGroupMember(member.getMemberId(), type); + sgm.setOrigin(member.getOrigin()); + return sgm; + } + + private boolean exists(String groupId, String memberId, String zoneId) { + Integer idResults = jdbcTemplate.queryForObject(GET_MEMBER_COUNT_SQL, Integer.class, memberId, groupId, zoneId); + return idResults != null && idResults == 1; + } + @Override public List updateOrAddMembers(String groupId, List members, String zoneId) throws ScimResourceNotFoundException { diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/user/JdbcUaaUserDatabase.java b/server/src/main/java/org/cloudfoundry/identity/uaa/user/JdbcUaaUserDatabase.java index 2db4176aced..501ef3d47e9 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/user/JdbcUaaUserDatabase.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/user/JdbcUaaUserDatabase.java @@ -49,6 +49,7 @@ public class JdbcUaaUserDatabase implements UaaUserDatabase { private int maxSqlParameters; private final RowMapper mapper = new UaaUserRowMapper(); + private final RowMapper minimalMapper = new UaaUserPrototypeRowMapper(); private final RowMapper userInfoMapper = new UserInfoRowMapper(); RowMapper getMapper() { @@ -84,6 +85,16 @@ public UaaUser retrieveUserByName(String username, String origin) throws Usernam } } + @Override + public UaaUserPrototype retrieveUserPrototypeByName(String username, String origin) throws UsernameNotFoundException { + try { + String sql = caseInsensitive ? DEFAULT_CASE_INSENSITIVE_USER_BY_USERNAME_QUERY : DEFAULT_CASE_SENSITIVE_USER_BY_USERNAME_QUERY; + return jdbcTemplate.queryForObject(sql, minimalMapper, username.toLowerCase(Locale.US), true, origin, identityZoneManager.getCurrentIdentityZoneId()); + } catch (EmptyResultDataAccessException e) { + throw new UsernameNotFoundException(username); + } + } + @Override public UaaUser retrieveUserById(String id) throws UsernameNotFoundException { try { @@ -93,6 +104,15 @@ public UaaUser retrieveUserById(String id) throws UsernameNotFoundException { } } + @Override + public UaaUserPrototype retrieveUserPrototypeById(String id) throws UsernameNotFoundException { + try { + return jdbcTemplate.queryForObject(DEFAULT_USER_BY_ID_QUERY, minimalMapper, id, true, identityZoneManager.getCurrentIdentityZoneId()); + } catch (EmptyResultDataAccessException e) { + throw new UsernameNotFoundException(id); + } + } + @Override public UaaUser retrieveUserByEmail(String email, String origin) throws UsernameNotFoundException { String sql = caseInsensitive ? DEFAULT_CASE_INSENSITIVE_USER_BY_EMAIL_AND_ORIGIN_QUERY : DEFAULT_CASE_SENSITIVE_USER_BY_EMAIL_AND_ORIGIN_QUERY; @@ -106,6 +126,19 @@ public UaaUser retrieveUserByEmail(String email, String origin) throws UsernameN } } + @Override + public UaaUserPrototype retrieveUserPrototypeByEmail(String email, String origin) throws UsernameNotFoundException { + String sql = caseInsensitive ? DEFAULT_CASE_INSENSITIVE_USER_BY_EMAIL_AND_ORIGIN_QUERY : DEFAULT_CASE_SENSITIVE_USER_BY_EMAIL_AND_ORIGIN_QUERY; + List results = jdbcTemplate.query(sql, minimalMapper, email.toLowerCase(Locale.US), true, origin, identityZoneManager.getCurrentIdentityZoneId()); + if (results.size() == 0) { + return null; + } else if (results.size() == 1) { + return results.get(0); + } else { + throw new IncorrectResultSizeDataAccessException(String.format("Multiple users match email=%s origin=%s", email, origin), 1, results.size()); + } + } + @Override public UserInfo getUserInfo(String id) { try { @@ -139,6 +172,39 @@ public void updateLastLogonTime(String userId) { jdbcTemplate.update(DEFAULT_UPDATE_USER_LAST_LOGON, timeService.getCurrentTimeMillis(), userId, identityZoneManager.getCurrentIdentityZoneId()); } + private UaaUserPrototype getUaaUserPrototype(ResultSet rs) throws SQLException { + String id = rs.getString("id"); + UaaUserPrototype prototype = new UaaUserPrototype().withId(id) + .withUsername(rs.getString("username")) + .withPassword(rs.getString("password")) + .withEmail(rs.getString("email")) + .withGivenName(rs.getString("givenName")) + .withFamilyName(rs.getString("familyName")) + .withCreated(rs.getTimestamp("created")) + .withModified(rs.getTimestamp("lastModified")) + .withOrigin(rs.getString("origin")) + .withExternalId(rs.getString("external_id")) + .withVerified(rs.getBoolean("verified")) + .withZoneId(rs.getString("identity_zone_id")) + .withSalt(rs.getString("salt")) + .withPasswordLastModified(rs.getTimestamp("passwd_lastmodified")) + .withPhoneNumber(rs.getString("phoneNumber")) + .withLegacyVerificationBehavior(rs.getBoolean("legacy_verification_behavior")) + .withPasswordChangeRequired(rs.getBoolean("passwd_change_required")); + + Long lastLogon = rs.getLong("last_logon_success_time"); + if (rs.wasNull()) { + lastLogon = null; + } + Long previousLogon = rs.getLong("previous_logon_success_time"); + if (rs.wasNull()) { + previousLogon = null; + } + prototype.withLastLogonSuccess(lastLogon) + .withPreviousLogonSuccess(previousLogon); + return prototype; + } + private final class UserInfoRowMapper implements RowMapper { @Override public UserInfo mapRow(ResultSet rs, int rowNum) throws SQLException { @@ -147,45 +213,21 @@ public UserInfo mapRow(ResultSet rs, int rowNum) throws SQLException { } } - private final class UaaUserRowMapper implements RowMapper { + private final class UaaUserPrototypeRowMapper implements RowMapper { @Override + public UaaUserPrototype mapRow(ResultSet rs, int rowNum) throws SQLException { + return getUaaUserPrototype(rs); + } + } - public UaaUser mapRow(ResultSet rs, int rowNum) throws SQLException { - String id = rs.getString("id"); - UaaUserPrototype prototype = new UaaUserPrototype().withId(id) - .withUsername(rs.getString("username")) - .withPassword(rs.getString("password")) - .withEmail(rs.getString("email")) - .withGivenName(rs.getString("givenName")) - .withFamilyName(rs.getString("familyName")) - .withCreated(rs.getTimestamp("created")) - .withModified(rs.getTimestamp("lastModified")) - .withOrigin(rs.getString("origin")) - .withExternalId(rs.getString("external_id")) - .withVerified(rs.getBoolean("verified")) - .withZoneId(rs.getString("identity_zone_id")) - .withSalt(rs.getString("salt")) - .withPasswordLastModified(rs.getTimestamp("passwd_lastmodified")) - .withPhoneNumber(rs.getString("phoneNumber")) - .withLegacyVerificationBehavior(rs.getBoolean("legacy_verification_behavior")) - .withPasswordChangeRequired(rs.getBoolean("passwd_change_required")); - - Long lastLogon = rs.getLong("last_logon_success_time"); - if (rs.wasNull()) { - lastLogon = null; + private final class UaaUserRowMapper implements RowMapper { + @Override + public UaaUser mapRow(ResultSet rs, int rowNum) throws SQLException { + UaaUserPrototype prototype = getUaaUserPrototype(rs); + List authorities = + AuthorityUtils.commaSeparatedStringToAuthorityList(getAuthorities(prototype.getId())); + return new UaaUser(prototype.withAuthorities(authorities)); } - Long previousLogon = rs.getLong("previous_logon_success_time"); - if (rs.wasNull()) { - previousLogon = null; - } - prototype.withLastLogonSuccess(lastLogon) - .withPreviousLogonSuccess(previousLogon); - - - List authorities = - AuthorityUtils.commaSeparatedStringToAuthorityList(getAuthorities(id)); - return new UaaUser(prototype.withAuthorities(authorities)); - } private String getAuthorities(final String userId) { Set authorities = new HashSet<>(); diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/user/UaaUser.java b/server/src/main/java/org/cloudfoundry/identity/uaa/user/UaaUser.java index be7363c0f56..b911c6684cb 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/user/UaaUser.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/user/UaaUser.java @@ -9,6 +9,7 @@ import java.util.Collection; import java.util.Date; import java.util.List; +import java.util.Optional; import java.util.function.Consumer; /** @@ -208,7 +209,7 @@ public String getSalt() { } public List getAuthorities() { - return authorities; + return Optional.ofNullable(authorities).orElseThrow(); } public UaaUser id(String id) { diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/user/UaaUserDatabase.java b/server/src/main/java/org/cloudfoundry/identity/uaa/user/UaaUserDatabase.java index 39c780ec3bf..39867a89e35 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/user/UaaUserDatabase.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/user/UaaUserDatabase.java @@ -20,10 +20,16 @@ public interface UaaUserDatabase { UaaUser retrieveUserByName(String username, String origin) throws UsernameNotFoundException; + UaaUserPrototype retrieveUserPrototypeByName(String username, String origin) throws UsernameNotFoundException; + UaaUser retrieveUserById(String id) throws UsernameNotFoundException; + UaaUserPrototype retrieveUserPrototypeById(String id) throws UsernameNotFoundException; + UaaUser retrieveUserByEmail(String email, String origin) throws UsernameNotFoundException; + UaaUserPrototype retrieveUserPrototypeByEmail(String email, String origin) throws UsernameNotFoundException; + UserInfo getUserInfo(String id); UserInfo storeUserInfo(String id, UserInfo info); diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/util/SessionUtils.java b/server/src/main/java/org/cloudfoundry/identity/uaa/util/SessionUtils.java index 31c738d7782..86e93be742e 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/util/SessionUtils.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/util/SessionUtils.java @@ -24,6 +24,7 @@ public final class SessionUtils { public static final String SPRING_SECURITY_CONTEXT = "SPRING_SECURITY_CONTEXT"; private static final String EXTERNAL_OAUTH_STATE_ATTRIBUTE_PREFIX = "external-oauth-state-"; + private static final String EXTERNAL_OAUTH_CODE_VERIFIER_ATTRIBUTE_PREFIX = "external-oauth-verifier-"; private SessionUtils() {} @@ -80,4 +81,8 @@ public static AuthenticationException getAuthenticationException(HttpSession ses public static String stateParameterAttributeKeyForIdp(String idpOriginKey) { return EXTERNAL_OAUTH_STATE_ATTRIBUTE_PREFIX + idpOriginKey; } + + public static String codeVerifierParameterAttributeKeyForIdp(String idpOriginKey) { + return EXTERNAL_OAUTH_CODE_VERIFIER_ATTRIBUTE_PREFIX + idpOriginKey; + } } diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/util/TokenValidation.java b/server/src/main/java/org/cloudfoundry/identity/uaa/util/TokenValidation.java index 77945f7b439..ba1b9a9cc88 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/util/TokenValidation.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/util/TokenValidation.java @@ -140,11 +140,11 @@ public TokenValidation checkIssuer(String issuer) { } protected TokenValidation checkExpiry(Instant asOf) { - if (!claims.containsKey(EXP)) { + if (!claims.containsKey(EXPIRY_IN_SECONDS)) { throw new InvalidTokenException("Token does not bear an EXP claim.", null); } - Object expClaim = claims.get(EXP); + Object expClaim = claims.get(EXPIRY_IN_SECONDS); long expiry; try { expiry = (int) expClaim; diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/util/UaaUrlUtils.java b/server/src/main/java/org/cloudfoundry/identity/uaa/util/UaaUrlUtils.java index a44d98ca66c..b813f99bd39 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/util/UaaUrlUtils.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/util/UaaUrlUtils.java @@ -1,6 +1,8 @@ package org.cloudfoundry.identity.uaa.util; import org.cloudfoundry.identity.uaa.zone.IdentityZone; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.util.AntPathMatcher; import org.springframework.util.MultiValueMap; import org.springframework.util.StringUtils; @@ -12,11 +14,14 @@ import javax.servlet.http.HttpServletRequest; import java.net.MalformedURLException; import java.net.URI; +import java.net.URISyntaxException; import java.net.URL; +import java.nio.charset.StandardCharsets; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.StringTokenizer; import java.util.regex.Pattern; import static java.util.Collections.emptyList; @@ -25,11 +30,14 @@ import static org.springframework.util.StringUtils.isEmpty; public abstract class UaaUrlUtils { - /** Pattern that matches valid subdomains. * According to https://tools.ietf.org/html/rfc3986#section-3.2.2 */ private static final Pattern VALID_SUBDOMAIN_PATTERN = Pattern.compile("([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\\-]*[a-zA-Z0-9])"); + private static final Logger s_logger = LoggerFactory.getLogger( + UaaUrlUtils.class); + + private static final int MAX_URI_DECODES = 5; public static String getUaaUrl(String path, IdentityZone currentIdentityZone) { return getUaaUrl(path, false, currentIdentityZone); @@ -98,13 +106,58 @@ public static String findMatchingRedirectUri(Collection redirectUris, St for (String pattern : ofNullable(redirectUris).orElse(emptyList())) { if (matcher.match(pattern, requestedRedirectUri)) { - return requestedRedirectUri; + if ( (!pattern.contains("*") && !pattern.contains("?")) || matchHost(pattern, requestedRedirectUri, matcher)) { + return requestedRedirectUri; + } + else { + s_logger.warn( + "The URI pattern matched but the hostname pattern did not. Denying the requested redirect URI: whitelisted-pattern='{}' requested-redirect-uri='{}'", + pattern, requestedRedirectUri); + } } } return ofNullable(fallbackRedirectUri).orElse(requestedRedirectUri); } + /** + * Retrieve hostname parts from uriPattern and + * requestedUri, then do Ant path match of the hostname parts. + */ + static boolean matchHost(String uriPattern, String requestedUri, AntPathMatcher matcher) { + requestedUri = requestedUri.replace('\\', '/'); + String hostnameFromRequestedUri; + try { + hostnameFromRequestedUri = new URI(requestedUri).getHost(); + } + catch (URISyntaxException ex) { + return false; + } + if (hostnameFromRequestedUri == null) { + // No URI scheme, likely relative URI, so just return true + return true; + } + + StringTokenizer st = new StringTokenizer(uriPattern, "/"); + String hostnameFromPattern = null; + while (st.hasMoreTokens()) { + String currentToken = st.nextToken(); + if (currentToken.endsWith(":")) { + continue; + } + hostnameFromPattern = currentToken; + break; + } + if (hostnameFromPattern == null) return false; + + int colonLocation = hostnameFromPattern.indexOf(':'); + if (colonLocation > 0) { + hostnameFromPattern = hostnameFromPattern.substring(0, colonLocation); + } + + return matcher.match(hostnameFromPattern, hostnameFromRequestedUri); + } + public static String getHostForURI(String uri) { return UriComponentsBuilder.fromHttpUrl(uri).build().getHost(); } @@ -230,10 +283,30 @@ public static String normalizeUri(String uri) { try { uriComponentsBuilder.host(nonNormalizedUri.getHost().toLowerCase()); uriComponentsBuilder.scheme(nonNormalizedUri.getScheme().toLowerCase()); + uriComponentsBuilder.replacePath(decodeUriPath(nonNormalizedUri.getPath())); } catch (NullPointerException e) { throw new IllegalArgumentException("URI host and scheme must not be null"); } return uriComponentsBuilder.build().toString(); } + + private static String decodeUriPath(final String path) { + if (path == null) { + return null; + } + + String normalizedPath = path; + for (int i = 0; i <= MAX_URI_DECODES; ++i) { + // This loop can run up to (MAX_URI_DECODES + 1) times. + // The extra iteration is used to check that the URI remains unchanged after decoding. + String normalizedPathPrev = normalizedPath; + normalizedPath = StringUtils.uriDecode(normalizedPath, StandardCharsets.UTF_8); + if (normalizedPath.equals(normalizedPathPrev)) { + return StringUtils.cleanPath(normalizedPath); + } + } + + throw new IllegalArgumentException("Aborted url decoding for repeatedly encoded path"); + } } diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/zone/MultitenantJdbcClientDetailsService.java b/server/src/main/java/org/cloudfoundry/identity/uaa/zone/MultitenantJdbcClientDetailsService.java index 0d741b439bf..bc7e3922807 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/zone/MultitenantJdbcClientDetailsService.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/zone/MultitenantJdbcClientDetailsService.java @@ -11,7 +11,6 @@ import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.dao.DataIntegrityViolationException; -import org.springframework.dao.DuplicateKeyException; import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.dao.InvalidDataAccessResourceUsageException; import org.springframework.jdbc.core.JdbcTemplate; @@ -76,6 +75,9 @@ public class MultitenantJdbcClientDetailsService extends MultitenantClientServic private static final String DEFAULT_SELECT_STATEMENT = BASE_FIND_STATEMENT + " where client_id = ? and identity_zone_id = ?"; + private static final String SINGLE_SELECT_STATEMENT = + "select client_id from oauth_client_details where client_id = ? and identity_zone_id = ?"; + private static final String DEFAULT_INSERT_STATEMENT = "insert into oauth_client_details (" + CLIENT_FIELDS + ", client_id, identity_zone_id, created_by) values (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)"; @@ -129,11 +131,18 @@ public ClientDetails loadClientByClientId(String clientId, String zoneId) throws @Override public void addClientDetails(ClientDetails clientDetails, String zoneId) throws ClientAlreadyExistsException { - try { - jdbcTemplate.update(DEFAULT_INSERT_STATEMENT, getInsertClientDetailsFields(clientDetails, zoneId)); - } catch (DuplicateKeyException e) { - throw new ClientAlreadyExistsException("Client already exists: " + clientDetails.getClientId(), e); + if (exists(clientDetails.getClientId(), zoneId)) { + throw new ClientAlreadyExistsException("Client already exists: " + clientDetails.getClientId()); + } + jdbcTemplate.update(DEFAULT_INSERT_STATEMENT, getInsertClientDetailsFields(clientDetails, zoneId)); + } + + private boolean exists(String clientId, String zoneId) { + List idResults = jdbcTemplate.queryForList(SINGLE_SELECT_STATEMENT, String.class, clientId, zoneId); + if (idResults != null && idResults.size() == 1) { + return true; } + return false; } @Override diff --git a/server/src/main/resources/spring/login-ui.xml b/server/src/main/resources/spring/login-ui.xml index e8ef0a5b09e..b29ac98cca7 100644 --- a/server/src/main/resources/spring/login-ui.xml +++ b/server/src/main/resources/spring/login-ui.xml @@ -270,6 +270,7 @@ + @@ -451,6 +452,7 @@ ${smtp.auth:false} ${smtp.starttls:false} + ${smtp.sslprotocols:TLSv1.2} diff --git a/server/src/main/resources/templates/web/access_confirmation.html b/server/src/main/resources/templates/web/access_confirmation.html index 99e230ec0ba..7f55cd48f42 100644 --- a/server/src/main/resources/templates/web/access_confirmation.html +++ b/server/src/main/resources/templates/web/access_confirmation.html @@ -2,7 +2,7 @@ + layout:decorate="~{layouts/main}"> @@ -21,7 +21,7 @@

Cloudbees

Cloudbees has requested permission to access your account. If you do not recognize this application or - its URL, you should click deny. The application will not see your password. + its URL, you should click Deny. The application will not see your password.

    @@ -51,7 +51,7 @@

    Cloudbees

You can change your approval of permissions or revoke access for this application - at any time from account settings. By approving access, you agree to + at any time from the account settings. By approving access, you agree to Cloudbees's terms of service and privacy policy.

diff --git a/server/src/main/resources/templates/web/access_confirmation_error.html b/server/src/main/resources/templates/web/access_confirmation_error.html index 7d454fd650c..2fbe2f85337 100644 --- a/server/src/main/resources/templates/web/access_confirmation_error.html +++ b/server/src/main/resources/templates/web/access_confirmation_error.html @@ -1,5 +1,5 @@ - +
diff --git a/server/src/main/resources/templates/web/accounts/email_sent.html b/server/src/main/resources/templates/web/accounts/email_sent.html index 5b0d5084c10..389d37b61b0 100644 --- a/server/src/main/resources/templates/web/accounts/email_sent.html +++ b/server/src/main/resources/templates/web/accounts/email_sent.html @@ -1,7 +1,7 @@ + layout:decorate="~{layouts/main}"> diff --git a/server/src/main/resources/templates/web/accounts/link_prompt.html b/server/src/main/resources/templates/web/accounts/link_prompt.html index 145b18d69fa..27d34953a52 100644 --- a/server/src/main/resources/templates/web/accounts/link_prompt.html +++ b/server/src/main/resources/templates/web/accounts/link_prompt.html @@ -1,7 +1,7 @@ + layout:decorate="~{layouts/main}">

Create your account

diff --git a/server/src/main/resources/templates/web/accounts/new_activation_email.html b/server/src/main/resources/templates/web/accounts/new_activation_email.html index a77bc756f2b..7150431ff98 100644 --- a/server/src/main/resources/templates/web/accounts/new_activation_email.html +++ b/server/src/main/resources/templates/web/accounts/new_activation_email.html @@ -1,7 +1,7 @@ + layout:decorate="~{layouts/main}">

Create your account

diff --git a/server/src/main/resources/templates/web/approvals.html b/server/src/main/resources/templates/web/approvals.html index a72c4348b7d..6d392a12450 100644 --- a/server/src/main/resources/templates/web/approvals.html +++ b/server/src/main/resources/templates/web/approvals.html @@ -2,7 +2,7 @@ + layout:decorate="~{layouts/main}">