vendor: run make vendor-update

This commit is contained in:
Aliaksandr Valialkin 2023-07-07 00:05:50 -07:00
parent bf49efc11a
commit e2a2d64053
No known key found for this signature in database
GPG key ID: A72BEC6CD3D0DED1
523 changed files with 47513 additions and 7750 deletions

105
go.mod
View file

@ -3,8 +3,8 @@ module github.com/VictoriaMetrics/VictoriaMetrics
go 1.19
require (
cloud.google.com/go/storage v1.30.1
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.6.0
cloud.google.com/go/storage v1.31.0
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.6.1
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.0.0
github.com/VictoriaMetrics/fastcache v1.12.1
@ -13,57 +13,59 @@ require (
github.com/VictoriaMetrics/fasthttp v1.2.0
github.com/VictoriaMetrics/metrics v1.24.0
github.com/VictoriaMetrics/metricsql v0.56.2
github.com/aws/aws-sdk-go-v2 v1.18.0
github.com/aws/aws-sdk-go-v2/config v1.18.25
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.67
github.com/aws/aws-sdk-go-v2/service/s3 v1.33.1
github.com/aws/aws-sdk-go-v2 v1.18.1
github.com/aws/aws-sdk-go-v2/config v1.18.27
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.71
github.com/aws/aws-sdk-go-v2/service/s3 v1.36.0
github.com/cespare/xxhash/v2 v2.2.0
github.com/cheggaaa/pb/v3 v3.1.2
github.com/gogo/protobuf v1.3.2
github.com/golang/snappy v0.0.4
github.com/googleapis/gax-go/v2 v2.8.0
github.com/influxdata/influxdb v1.11.1
github.com/klauspost/compress v1.16.5
github.com/prometheus/prometheus v0.44.0
github.com/urfave/cli/v2 v2.25.3
github.com/googleapis/gax-go/v2 v2.12.0
github.com/influxdata/influxdb v1.11.2
github.com/klauspost/compress v1.16.7
github.com/prometheus/prometheus v0.45.0
github.com/urfave/cli/v2 v2.25.7
github.com/valyala/fastjson v1.6.4
github.com/valyala/fastrand v1.1.0
github.com/valyala/fasttemplate v1.2.2
github.com/valyala/gozstd v1.20.1
github.com/valyala/histogram v1.2.0
github.com/valyala/quicktemplate v1.7.0
golang.org/x/net v0.10.0
golang.org/x/oauth2 v0.8.0
golang.org/x/sys v0.8.0
google.golang.org/api v0.123.0
golang.org/x/net v0.12.0
golang.org/x/oauth2 v0.10.0
golang.org/x/sys v0.10.0
google.golang.org/api v0.130.0
gopkg.in/yaml.v2 v2.4.0
)
require github.com/bmatcuk/doublestar/v4 v4.6.0
require (
cloud.google.com/go v0.110.2 // indirect
cloud.google.com/go/compute v1.19.3 // indirect
cloud.google.com/go v0.110.4 // indirect
cloud.google.com/go/compute v1.20.1 // indirect
cloud.google.com/go/compute/metadata v0.2.3 // indirect
cloud.google.com/go/iam v1.0.1 // indirect
cloud.google.com/go/iam v1.1.1 // indirect
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0 // indirect
github.com/AzureAD/microsoft-authentication-library-for-go v1.0.0 // indirect
github.com/VividCortex/ewma v1.2.0 // indirect
github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 // indirect
github.com/aws/aws-sdk-go v1.44.265 // indirect
github.com/aws/aws-sdk-go v1.44.297 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.13.24 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.3 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.33 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.27 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.34 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.25 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.13.26 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.4 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.34 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.28 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.35 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.26 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.11 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.28 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.27 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.14.2 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.12.10 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.10 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.19.0 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.29 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.28 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.14.3 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.12.12 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.12 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.19.2 // indirect
github.com/aws/smithy-go v1.13.5 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
@ -75,49 +77,54 @@ require (
github.com/go-logfmt/logfmt v0.6.0 // indirect
github.com/go-logr/logr v1.2.4 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/google/go-cmp v0.5.9 // indirect
github.com/google/s2a-go v0.1.3 // indirect
github.com/google/s2a-go v0.1.4 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.2.3 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.2.5 // indirect
github.com/grafana/regexp v0.0.0-20221122212121-6b5c0a4cb7fd // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/jpillora/backoff v1.0.0 // indirect
github.com/kylelemons/godebug v1.1.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.18 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/mattn/go-runewidth v0.0.14 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect
github.com/oklog/ulid v1.3.1 // indirect
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_golang v1.15.1 // indirect
github.com/prometheus/client_golang v1.16.0 // indirect
github.com/prometheus/client_model v0.4.0 // indirect
github.com/prometheus/common v0.43.0 // indirect
github.com/prometheus/common v0.44.0 // indirect
github.com/prometheus/common/sigv4 v0.1.0 // indirect
github.com/prometheus/procfs v0.9.0 // indirect
github.com/prometheus/procfs v0.11.0 // indirect
github.com/rivo/uniseg v0.4.4 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/stretchr/testify v1.8.2 // indirect
github.com/stretchr/testify v1.8.4 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
go.opencensus.io v0.24.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.41.1 // indirect
go.opentelemetry.io/otel v1.15.1 // indirect
go.opentelemetry.io/otel/metric v0.38.1 // indirect
go.opentelemetry.io/otel/trace v1.15.1 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.42.0 // indirect
go.opentelemetry.io/otel v1.16.0 // indirect
go.opentelemetry.io/otel/metric v1.16.0 // indirect
go.opentelemetry.io/otel/trace v1.16.0 // indirect
go.uber.org/atomic v1.11.0 // indirect
go.uber.org/goleak v1.2.1 // indirect
golang.org/x/crypto v0.9.0 // indirect
golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc // indirect
golang.org/x/sync v0.2.0 // indirect
golang.org/x/text v0.9.0 // indirect
golang.org/x/crypto v0.11.0 // indirect
golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df // indirect
golang.org/x/sync v0.3.0 // indirect
golang.org/x/text v0.11.0 // indirect
golang.org/x/time v0.3.0 // indirect
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect
google.golang.org/grpc v1.55.0 // indirect
google.golang.org/protobuf v1.30.0 // indirect
google.golang.org/genproto v0.0.0-20230706204954-ccb25ca9f130 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20230706204954-ccb25ca9f130 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20230706204954-ccb25ca9f130 // indirect
google.golang.org/grpc v1.56.2 // indirect
google.golang.org/protobuf v1.31.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

253
go.sum
View file

@ -13,23 +13,22 @@ cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKV
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
cloud.google.com/go v0.110.2 h1:sdFPBr6xG9/wkBbfhmUz/JmZC7X6LavQgcrVINrKiVA=
cloud.google.com/go v0.110.2/go.mod h1:k04UEeEtb6ZBRTv3dZz4CeJC3jKGxyhl0sAiVVquxiw=
cloud.google.com/go v0.110.4 h1:1JYyxKMN9hd5dR2MYTPWkGUgcoxVVhg0LKNKEo0qvmk=
cloud.google.com/go v0.110.4/go.mod h1:+EYjdK8e5RME/VY/qLCAtuyALQ9q67dvuum8i+H5xsI=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
cloud.google.com/go/compute v1.19.3 h1:DcTwsFgGev/wV5+q8o2fzgcHOaac+DKGC91ZlvpsQds=
cloud.google.com/go/compute v1.19.3/go.mod h1:qxvISKp/gYnXkSAD1ppcSOveRAmzxicEv/JlizULFrI=
cloud.google.com/go/compute v1.20.1 h1:6aKEtlUiwEpJzM001l0yFkpXmUVXaN8W+fbkb2AZNbg=
cloud.google.com/go/compute v1.20.1/go.mod h1:4tCnrn48xsqlwSAiLf1HXMQk8CONslYbdiEZc9FEIbM=
cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY=
cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
cloud.google.com/go/iam v1.0.1 h1:lyeCAU6jpnVNrE9zGQkTl3WgNgK/X+uWwaw0kynZJMU=
cloud.google.com/go/iam v1.0.1/go.mod h1:yR3tmSL8BcZB4bxByRv2jkSIahVmCtfKZwLYGBalRE8=
cloud.google.com/go/longrunning v0.4.1 h1:v+yFJOfKC3yZdY6ZUI933pIYdhyhV8S3NpWrXWmg7jM=
cloud.google.com/go/iam v1.1.1 h1:lW7fzj15aVIXYHREOqjRBV9PsH0Z6u8Y46a1YGvQP4Y=
cloud.google.com/go/iam v1.1.1/go.mod h1:A5avdyVL2tCppe4unb0951eI9jreack+RJ0/d+KUZOU=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
@ -39,26 +38,28 @@ 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=
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
cloud.google.com/go/storage v1.30.1 h1:uOdMxAs8HExqBlnLtnQyP0YkvbiDpdGShGKtx6U/oNM=
cloud.google.com/go/storage v1.30.1/go.mod h1:NfxhC0UJE1aXSx7CIIbCf7y9HKT7BiccwkR7+P7gN8E=
cloud.google.com/go/storage v1.31.0 h1:+S3LjjEN2zZ+L5hOwj4+1OkGCsLVe0NzpXKQ1pSdTCI=
cloud.google.com/go/storage v1.31.0/go.mod h1:81ams1PrhW16L4kF7qg+4mTq7SRs5HsbDTM0bWvrwJ0=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/Azure/azure-sdk-for-go v65.0.0+incompatible h1:HzKLt3kIwMm4KeJYTdx9EbjRYTySD/t8i1Ee/W5EGXw=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.6.0 h1:8kDqDngH+DmVBiCtIjCFTGa7MBnsIOkF9IccInFEbjk=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.6.0/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.1.0 h1:QkAcEIAKbNL4KoFr4SathZPhDhF4mVwpBMFlYjyAqy8=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.6.1 h1:SEy2xmstIphdPwNBUi7uhvjyjhVKISfwjfOJmuy7kg4=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.6.1/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.0 h1:vcYCAze6p19qBW7MhZybIsqD8sMV8js0NyQM8JDnVtg=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.0/go.mod h1:OQeznEEkTZ9OrhHJoDD8ZDq51FHgXjqtP9z6bEwBq9U=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0 h1:sXr+ck84g/ZlZUOZiNELInmMgOsuGwdjjVkEIde0OtY=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0/go.mod h1:okt5dMMTOFjX/aovMlrjvvXoPMBVSPzk9185BT0+eZM=
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.0.0 h1:u/LLAOFgsMv7HmNL4Qufg58y+qElGOt5qv0z1mURkRY=
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.0.0/go.mod h1:2e8rMJtl2+2j+HXbTBwnyGpm5Nou7KhvSfxOq8JpTag=
github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs=
github.com/Azure/go-autorest/autorest v0.11.28 h1:ndAExarwr5Y+GaHE6VCaY1kyS/HwwGGyuimVhWsHOEM=
github.com/Azure/go-autorest/autorest v0.11.29 h1:I4+HL/JDvErx2LjyzaVxllw2lRDB5/BT2Bm4g20iqYw=
github.com/Azure/go-autorest/autorest/adal v0.9.23 h1:Yepx8CvFxwNKpH6ja7RZ+sKX+DWYNldbLiALMC3BTz8=
github.com/Azure/go-autorest/autorest/date v0.3.0 h1:7gUk1U5M/CQbp9WoqinNzJar+8KY+LPI6wiWrP/myHw=
github.com/Azure/go-autorest/autorest/to v0.4.0 h1:oXVqrxakqqV1UZdSazDOPOLvOIz+XA683u8EctwboHk=
github.com/Azure/go-autorest/autorest/validation v0.3.1 h1:AgyqjAd94fwNAoTjl/WQXg4VvFeRFpO+UhNyRXqF1ac=
github.com/Azure/go-autorest/logger v0.2.1 h1:IG7i4p/mDa2Ce4TRyAO8IHnVhAVF3RFU+ZtXWSmf4Tg=
github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo=
github.com/AzureAD/microsoft-authentication-library-for-go v0.5.1 h1:BWe8a+f/t+7KY7zH2mqygeUD0t8hNFXe08p1Pb3/jKE=
github.com/AzureAD/microsoft-authentication-library-for-go v1.0.0 h1:OBhqkivkhkMqLPymWEppkm7vgPQY2XsHoEkaMQ0AdZY=
github.com/AzureAD/microsoft-authentication-library-for-go v1.0.0/go.mod h1:kgDmCTgBzIEPFElEF+FK0SdjAor06dRq2Go927dnQ6o=
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=
github.com/Microsoft/go-winio v0.6.0 h1:slsWYD/zyx7lCXoZVlvQrj0hPTM1HI4+v1sIda2yDvg=
@ -87,44 +88,44 @@ github.com/andybalholm/brotli v1.0.3/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHG
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
github.com/armon/go-metrics v0.4.1 h1:hR91U9KYmb6bLBYLQjyM+3j+rcd/UhE+G78SFnF8gJA=
github.com/aws/aws-sdk-go v1.38.35/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro=
github.com/aws/aws-sdk-go v1.44.265 h1:rlBuD8OYjM5Vfcf7jDa264oVHqlPqY7y7o+JmrjNFUc=
github.com/aws/aws-sdk-go v1.44.265/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI=
github.com/aws/aws-sdk-go-v2 v1.18.0 h1:882kkTpSFhdgYRKVZ/VCgf7sd0ru57p2JCxz4/oN5RY=
github.com/aws/aws-sdk-go-v2 v1.18.0/go.mod h1:uzbQtefpm44goOPmdKyAlXSNcwlRgF3ePWVW6EtJvvw=
github.com/aws/aws-sdk-go v1.44.297 h1:uL4EV0gQxotQVYegIoBqK079328MOJqgG95daFYSkAM=
github.com/aws/aws-sdk-go v1.44.297/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI=
github.com/aws/aws-sdk-go-v2 v1.18.1 h1:+tefE750oAb7ZQGzla6bLkOwfcQCEtC5y2RqoqCeqKo=
github.com/aws/aws-sdk-go-v2 v1.18.1/go.mod h1:uzbQtefpm44goOPmdKyAlXSNcwlRgF3ePWVW6EtJvvw=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10 h1:dK82zF6kkPeCo8J1e+tGx4JdvDIQzj7ygIoLg8WMuGs=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10/go.mod h1:VeTZetY5KRJLuD/7fkQXMU6Mw7H5m/KP2J5Iy9osMno=
github.com/aws/aws-sdk-go-v2/config v1.18.25 h1:JuYyZcnMPBiFqn87L2cRppo+rNwgah6YwD3VuyvaW6Q=
github.com/aws/aws-sdk-go-v2/config v1.18.25/go.mod h1:dZnYpD5wTW/dQF0rRNLVypB396zWCcPiBIvdvSWHEg4=
github.com/aws/aws-sdk-go-v2/credentials v1.13.24 h1:PjiYyls3QdCrzqUN35jMWtUK1vqVZ+zLfdOa/UPFDp0=
github.com/aws/aws-sdk-go-v2/credentials v1.13.24/go.mod h1:jYPYi99wUOPIFi0rhiOvXeSEReVOzBqFNOX5bXYoG2o=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.3 h1:jJPgroehGvjrde3XufFIJUZVK5A2L9a3KwSFgKy9n8w=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.3/go.mod h1:4Q0UFP0YJf0NrsEuEYHpM9fTSEVnD16Z3uyEF7J9JGM=
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.67 h1:fI9/5BDEaAv/pv1VO1X1n3jfP9it+IGqWsCuuBQI8wM=
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.67/go.mod h1:zQClPRIwQZfJlZq6WZve+s4Tb4JW+3V6eS+4+KrYeP8=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.33 h1:kG5eQilShqmJbv11XL1VpyDbaEJzWxd4zRiCG30GSn4=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.33/go.mod h1:7i0PF1ME/2eUPFcjkVIwq+DOygHEoK92t5cDqNgYbIw=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.27 h1:vFQlirhuM8lLlpI7imKOMsjdQLuN9CPi+k44F/OFVsk=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.27/go.mod h1:UrHnn3QV/d0pBZ6QBAEQcqFLf8FAzLmoUfPVIueOvoM=
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.34 h1:gGLG7yKaXG02/jBlg210R7VgQIotiQntNhsCFejawx8=
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.34/go.mod h1:Etz2dj6UHYuw+Xw830KfzCfWGMzqvUTCjUj5b76GVDc=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.25 h1:AzwRi5OKKwo4QNqPf7TjeO+tK8AyOK3GVSwmRPo7/Cs=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.25/go.mod h1:SUbB4wcbSEyCvqBxv/O/IBf93RbEze7U7OnoTlpPB+g=
github.com/aws/aws-sdk-go-v2/config v1.18.27 h1:Az9uLwmssTE6OGTpsFqOnaGpLnKDqNYOJzWuC6UAYzA=
github.com/aws/aws-sdk-go-v2/config v1.18.27/go.mod h1:0My+YgmkGxeqjXZb5BYme5pc4drjTnM+x1GJ3zv42Nw=
github.com/aws/aws-sdk-go-v2/credentials v1.13.26 h1:qmU+yhKmOCyujmuPY7tf5MxR/RKyZrOPO3V4DobiTUk=
github.com/aws/aws-sdk-go-v2/credentials v1.13.26/go.mod h1:GoXt2YC8jHUBbA4jr+W3JiemnIbkXOfxSXcisUsZ3os=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.4 h1:LxK/bitrAr4lnh9LnIS6i7zWbCOdMsfzKFBI6LUCS0I=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.4/go.mod h1:E1hLXN/BL2e6YizK1zFlYd8vsfi2GTjbjBazinMmeaM=
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.71 h1:SAB1UAVaf6nGCu3zyIrV+VWsendXrms1GqtW4zBotKA=
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.71/go.mod h1:ZNo5H4PR3/fwsXYqb+Ld5YAfvHcYCbltaTTtSay4l2o=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.34 h1:A5UqQEmPaCFpedKouS4v+dHCTUo2sKqhoKO9U5kxyWo=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.34/go.mod h1:wZpTEecJe0Btj3IYnDx/VlUzor9wm3fJHyvLpQF0VwY=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.28 h1:srIVS45eQuewqz6fKKu6ZGXaq6FuFg5NzgQBAM6g8Y4=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.28/go.mod h1:7VRpKQQedkfIEXb4k52I7swUnZP0wohVajJMRn3vsUw=
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.35 h1:LWA+3kDM8ly001vJ1X1waCuLJdtTl48gwkPKWy9sosI=
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.35/go.mod h1:0Eg1YjxE0Bhn56lx+SHJwCzhW+2JGtizsrx+lCqrfm0=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.26 h1:wscW+pnn3J1OYnanMnza5ZVYXLX4cKk5rAvUAl4Qu+c=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.26/go.mod h1:MtYiox5gvyB+OyP0Mr0Sm/yzbEAIPL9eijj/ouHAPw0=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.11 h1:y2+VQzC6Zh2ojtV2LoC0MNwHWc6qXv/j2vrQtlftkdA=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.11/go.mod h1:iV4q2hsqtNECrfmlXyord9u4zyuFEJX9eLgLpSPzWA8=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.28 h1:vGWm5vTpMr39tEZfQeDiDAMgk+5qsnvRny3FjLpnH5w=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.28/go.mod h1:spfrICMD6wCAhjhzHuy6DOZZ+LAIY10UxhUmLzpJTTs=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.27 h1:0iKliEXAcCa2qVtRs7Ot5hItA2MsufrphbRFlz1Owxo=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.27/go.mod h1:EOwBD4J4S5qYszS5/3DpkejfuK+Z5/1uzICfPaZLtqw=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.14.2 h1:NbWkRxEEIRSCqxhsHQuMiTH7yo+JZW1gp8v3elSVMTQ=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.14.2/go.mod h1:4tfW5l4IAB32VWCDEBxCRtR9T4BWy4I4kr1spr8NgZM=
github.com/aws/aws-sdk-go-v2/service/s3 v1.33.1 h1:O+9nAy9Bb6bJFTpeNFtd9UfHbgxO1o4ZDAM9rQp5NsY=
github.com/aws/aws-sdk-go-v2/service/s3 v1.33.1/go.mod h1:J9kLNzEiHSeGMyN7238EjJmBpCniVzFda75Gxl/NqB8=
github.com/aws/aws-sdk-go-v2/service/sso v1.12.10 h1:UBQjaMTCKwyUYwiVnUt6toEJwGXsLBI6al083tpjJzY=
github.com/aws/aws-sdk-go-v2/service/sso v1.12.10/go.mod h1:ouy2P4z6sJN70fR3ka3wD3Ro3KezSxU6eKGQI2+2fjI=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.10 h1:PkHIIJs8qvq0e5QybnZoG1K/9QTrLr9OsqCIo59jOBA=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.10/go.mod h1:AFvkxc8xfBe8XA+5St5XIHHrQQtkxqrRincx4hmMHOk=
github.com/aws/aws-sdk-go-v2/service/sts v1.19.0 h1:2DQLAKDteoEDI8zpCzqBMaZlJuoE9iTYD0gFmXVax9E=
github.com/aws/aws-sdk-go-v2/service/sts v1.19.0/go.mod h1:BgQOMsg8av8jset59jelyPW7NoZcZXLVpDsXunGDrk8=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.29 h1:zZSLP3v3riMOP14H7b4XP0uyfREDQOYv2cqIrvTXDNQ=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.29/go.mod h1:z7EjRjVwZ6pWcWdI2H64dKttvzaP99jRIj5hphW0M5U=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.28 h1:bkRyG4a929RCnpVSTvLM2j/T4ls015ZhhYApbmYs15s=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.28/go.mod h1:jj7znCIg05jXlaGBlFMGP8+7UN3VtCkRBG2spnmRQkU=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.14.3 h1:dBL3StFxHtpBzJJ/mNEsjXVgfO+7jR0dAIEwLqMapEA=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.14.3/go.mod h1:f1QyiAsvIv4B49DmCqrhlXqyaR+0IxMmyX+1P+AnzOM=
github.com/aws/aws-sdk-go-v2/service/s3 v1.36.0 h1:lEmQ1XSD9qLk+NZXbgvLJI/IiTz7OIR2TYUTFH25EI4=
github.com/aws/aws-sdk-go-v2/service/s3 v1.36.0/go.mod h1:aVbf0sko/TsLWHx30c/uVu7c62+0EAJ3vbxaJga0xCw=
github.com/aws/aws-sdk-go-v2/service/sso v1.12.12 h1:nneMBM2p79PGWBQovYO/6Xnc2ryRMw3InnDJq1FHkSY=
github.com/aws/aws-sdk-go-v2/service/sso v1.12.12/go.mod h1:HuCOxYsF21eKrerARYO6HapNeh9GBNq7fius2AcwodY=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.12 h1:2qTR7IFk7/0IN/adSFhYu9Xthr0zVFTgBrmPldILn80=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.12/go.mod h1:E4VrHCPzmVB/KFXtqBGKb3c8zpbNBgKe3fisDNLAW5w=
github.com/aws/aws-sdk-go-v2/service/sts v1.19.2 h1:XFJ2Z6sNUUcAz9poj+245DMkrHE4h2j5I9/xD50RHfE=
github.com/aws/aws-sdk-go-v2/service/sts v1.19.2/go.mod h1:dp0yLPsLBOi++WTxzCjA/oZqi6NPIhoR+uF7GeMU9eg=
github.com/aws/smithy-go v1.13.5 h1:hgz0X/DX0dGqTYpGALqXJoRKRj5oQ7150i5FdTePzO8=
github.com/aws/smithy-go v1.13.5/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
@ -149,7 +150,7 @@ github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XP
github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20230310173818-32f1caf87195 h1:58f1tJ1ra+zFINPlwLWvQsR9CzAKt2e+EWV2yX9oXQ4=
github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4 h1:/inchEIKaYC1Akx+H+gqO04wryn5h75LSazbRlnya1k=
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@ -157,10 +158,10 @@ 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/dennwc/varint v1.0.0 h1:kGNFFSSw8ToIy3obO/kKr8U9GZYUAxQEVuix4zfDWzE=
github.com/dennwc/varint v1.0.0/go.mod h1:hnItb35rvZvJrbTALZtY/iQfDs48JKRG1RPpgziApxA=
github.com/digitalocean/godo v1.98.0 h1:potyC1eD0N9n5/P4/WmJuKgg+OGYZOBWEW+/aKTX6QQ=
github.com/dnaeon/go-vcr v1.1.0 h1:ReYa/UBrRyQdant9B4fNHGoCNKw6qh6P0fsdGmZpR7c=
github.com/digitalocean/godo v1.99.0 h1:gUHO7n9bDaZFWvbzOum4bXE0/09ZuYA9yA8idQHX57E=
github.com/dnaeon/go-vcr v1.2.0 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI=
github.com/docker/distribution v2.8.1+incompatible h1:Q50tZOPR6T/hjNsyc9g8/syEs6bk8XXApsHjKukMl68=
github.com/docker/docker v23.0.4+incompatible h1:Kd3Bh9V/rO+XpTP/BLqM+gx8z7+Yb0AA2Ibj+nNo4ek=
github.com/docker/docker v24.0.2+incompatible h1:eATx+oLz9WdNVkQrr0qjQ8HvRJ4bOOxfzEo8R+dA3cg=
github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/edsrzf/mmap-go v1.1.0 h1:6EUwBLQ/Mcr1EYLE4Tn1VdW1A4ckqCQWZBw8Hr0kjpQ=
@ -170,9 +171,9 @@ github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.m
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=
github.com/envoyproxy/go-control-plane v0.11.0 h1:jtLewhRR2vMRNnq2ZZUoCjUlgut+Y0+sDDWPOfwOi1o=
github.com/envoyproxy/go-control-plane v0.11.1-0.20230524094728-9239064ad72f h1:7T++XKzy4xg7PKy+bM+Sa9/oe1OC88yz2hXQUISoXfA=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/envoyproxy/protoc-gen-validate v0.10.1 h1:c0g45+xCJhdgFGw7a5QAfdS4byAbud7miNWJ1WwEVf8=
github.com/envoyproxy/protoc-gen-validate v1.0.1 h1:kt9FtLiooDc0vbwTLhdg3dyNX1K9Qwa1EK9LcD4jVUQ=
github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs=
github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw=
github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk=
@ -206,8 +207,8 @@ github.com/go-zookeeper/zk v1.0.3 h1:7M2kwOsc//9VeeFiPtf+uSJlVpU66x9Ba5+8XK7/TDg
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-jwt/jwt v3.2.1+incompatible h1:73Z+4BJcrTC+KczS6WvTPvRGOp1WmfEP4Q1lOd9Z/+c=
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
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=
@ -273,23 +274,23 @@ github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hf
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/s2a-go v0.1.3 h1:FAgZmpLl/SXurPEZyCMPBIiiYeTbqfjlbdnCNTAkbGE=
github.com/google/s2a-go v0.1.3/go.mod h1:Ej+mSEMGRnqRzjc7VtF+jdBwYG5fuJfiZ8ELkjEwM0A=
github.com/google/s2a-go v0.1.4 h1:1kZ/sQM3srePvKs3tXAvQzo66XfcReoqFpIpIccE7Oc=
github.com/google/s2a-go v0.1.4/go.mod h1:Ej+mSEMGRnqRzjc7VtF+jdBwYG5fuJfiZ8ELkjEwM0A=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/enterprise-certificate-proxy v0.2.3 h1:yk9/cqRKtT9wXZSsRH9aurXEpJX+U6FLtpYTdC3R06k=
github.com/googleapis/enterprise-certificate-proxy v0.2.3/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k=
github.com/googleapis/enterprise-certificate-proxy v0.2.5 h1:UR4rDjcgpgEnqpIEvkiqTYKBCKLNmlge2eVjoZfySzM=
github.com/googleapis/enterprise-certificate-proxy v0.2.5/go.mod h1:RxW0N9901Cko1VOCW3SXCpWP+mlIEkk2tP7jnHy9a3w=
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/gax-go/v2 v2.8.0 h1:UBtEZqx1bjXtOQ5BVTkuYghXrr3N4V123VKJK67vJZc=
github.com/googleapis/gax-go/v2 v2.8.0/go.mod h1:4orTrqY6hXxxaUL4LHIPl6lGo8vAE38/qKbhSAKP6QI=
github.com/gophercloud/gophercloud v1.3.0 h1:RUKyCMiZoQR3VlVR5E3K7PK1AC3/qppsWYo6dtBiqs8=
github.com/googleapis/gax-go/v2 v2.12.0 h1:A+gCJKdRfqXkr+BIRGtZLibNXf0m1f9E4HG56etFpas=
github.com/googleapis/gax-go/v2 v2.12.0/go.mod h1:y+aIqrI5eb1YGMVJfuV3185Ts/D7qKpsEkdD5+I6QGU=
github.com/gophercloud/gophercloud v1.4.0 h1:RqEu43vaX0lb0LanZr5BylK5ICVxjpFFoc0sxivyuHU=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/grafana/regexp v0.0.0-20221122212121-6b5c0a4cb7fd h1:PpuIBO5P3e9hpqBD0O/HjhShYuM6XE0i/lbE6J94kww=
github.com/grafana/regexp v0.0.0-20221122212121-6b5c0a4cb7fd/go.mod h1:M5qHK+eWfAv8VR/265dIuEpL3fNfeC21tXXp9itM24A=
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
github.com/hashicorp/consul/api v1.20.0 h1:9IHTjNVSZ7MIwjlW3N3a7iGiykCMDpxZu8jsxFJh0yc=
github.com/hashicorp/consul/api v1.21.0 h1:WMR2JiyuaQWRAMFaOGiYfY4Q4HRpyYRe/oYQofjyduM=
github.com/hashicorp/cronexpr v1.1.1 h1:NJZDd87hGXjoZBdvyCF9mX4DCq5Wy7+A/w+A7q0wn6c=
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
@ -301,14 +302,14 @@ github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5O
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/hashicorp/golang-lru v0.6.0 h1:uL2shRDx7RTrOrTCUZEGP/wJUFiUI8QT6E7z5o8jga4=
github.com/hashicorp/nomad/api v0.0.0-20230418003350-3067191c5197 h1:I5xhKLePXpXgM6pZ4xZNTiurLLS3sGuZrZFFzAbM67A=
github.com/hashicorp/nomad/api v0.0.0-20230605233119-67e39d5d248f h1:yxjcAZRuYymIDC0W4IQHgTe9EQdu2BsjPlVmKwyVZT4=
github.com/hashicorp/serf v0.10.1 h1:Z1H2J60yRKvfDYAOZLd2MU0ND4AH/WDz7xYHDWQsIPY=
github.com/hetznercloud/hcloud-go v1.42.0 h1:Es/CDOForQN3nOOP5Vxh1N/YHjpCg386iYEX5zCgi+A=
github.com/hetznercloud/hcloud-go v1.45.1 h1:nl0OOklFfQT5J6AaNIOhl5Ruh3fhmGmhvZEqHbibVuk=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk=
github.com/influxdata/influxdb v1.11.1 h1:VEkQVMJ83gjpyS2FJuQaSbt4Mu+btGBoZbVq0XwTHGQ=
github.com/influxdata/influxdb v1.11.1/go.mod h1:WSTwm8ZvJARODSZJfcxdghcjCQVstHwClgO6MrbnGt0=
github.com/ionos-cloud/sdk-go/v6 v6.1.6 h1:0n4irdqNska+1s3YMCRhrAqKbibEgQ7SwwhAlHzYT5A=
github.com/influxdata/influxdb v1.11.2 h1:qOF3uQN1mDfJNEKwbAgJsqehf8IXgKok2vlGm736oGo=
github.com/influxdata/influxdb v1.11.2/go.mod h1:eUMkLTE2vQwvSk6KGMrTBLKPaqSuczuelGbggigMPFw=
github.com/ionos-cloud/sdk-go/v6 v6.1.7 h1:uVG1Q/ZDJ7YmCI9Oevpue9xJEH5UrUMyXv8gm7NTxIw=
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
@ -328,8 +329,8 @@ github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.13.4/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=
github.com/klauspost/compress v1.13.5/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/klauspost/compress v1.16.5 h1:IFV2oUNUzZaz+XyusxpLzpzS8Pt5rh0Z16For/djlyI=
github.com/klauspost/compress v1.16.5/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I=
github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b h1:udzkj9S/zlT5X367kqJis0QP7YMxobob6zhzq6Yre00=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
@ -340,19 +341,20 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
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/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/linode/linodego v1.16.1 h1:5otq57M4PdHycPERRfSFZ0s1yz1ETVWGjCp3hh7+F9w=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/linode/linodego v1.17.0 h1:aWS98f0jUoY2lhsEuBxRdVkqyGM0nazPd68AEDF0EvU=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98=
github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
github.com/miekg/dns v1.1.53 h1:ZBkuHr5dxHtB1caEOlZTLPo7D3L3TWckgUUs/RHfDxw=
github.com/miekg/dns v1.1.54 h1:5jon9mWcb0sFJGpnI99tOMhCPyJ+RPVz5b63MQG0VWI=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@ -371,6 +373,7 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8
github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM=
github.com/ovh/go-ovh v1.4.1 h1:VBGa5wMyQtTP7Zb+w97zRCh9sLtM/2YKRyy+MEJmWaM=
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU=
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
@ -381,8 +384,8 @@ github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXP
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
github.com/prometheus/client_golang v1.15.1 h1:8tXpTmJbyH5lydzFPoxSIJ0J46jdh3tylbvM1xCv0LI=
github.com/prometheus/client_golang v1.15.1/go.mod h1:e9yaBhRPU2pPNsZwE+JdQl0KEt1N9XgF6zxWmaC0xOk=
github.com/prometheus/client_golang v1.16.0 h1:yk/hx9hDbrGHovbci4BY+pRMfSuuat626eFsHb7tmT8=
github.com/prometheus/client_golang v1.16.0/go.mod h1:Zsulrv/L9oM40tJ7T815tM89lFEugiJ9HzIqaAx4LKc=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
@ -393,18 +396,18 @@ github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y8
github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=
github.com/prometheus/common v0.29.0/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls=
github.com/prometheus/common v0.43.0 h1:iq+BVjvYLei5f27wiuNiB1DN6DYQkp1c8Bx0Vykh5us=
github.com/prometheus/common v0.43.0/go.mod h1:NCvr5cQIh3Y/gy73/RdVtC9r8xxrxwJnB+2lB3BxrFc=
github.com/prometheus/common v0.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdOOfY=
github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY=
github.com/prometheus/common/sigv4 v0.1.0 h1:qoVebwtwwEhS85Czm2dSROY5fTo2PAPEVdDeppTwGX4=
github.com/prometheus/common/sigv4 v0.1.0/go.mod h1:2Jkxxk9yYvCkE5G1sQT7GuEXm57JrvHu9k5YwTjsNtI=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI=
github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY=
github.com/prometheus/prometheus v0.44.0 h1:sgn8Fdx+uE5tHQn0/622swlk2XnIj6udoZCnbVjHIgc=
github.com/prometheus/prometheus v0.44.0/go.mod h1:aPsmIK3py5XammeTguyqTmuqzX/jeCdyOWWobLHNKQg=
github.com/prometheus/procfs v0.11.0 h1:5EAgkfkMl659uZPbe9AS2N68a7Cc1TJbPEuGzFuRbyk=
github.com/prometheus/procfs v0.11.0/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM=
github.com/prometheus/prometheus v0.45.0 h1:O/uG+Nw4kNxx/jDPxmjsSDd+9Ohql6E7ZSY1x5x/0KI=
github.com/prometheus/prometheus v0.45.0/go.mod h1:jC5hyO8ItJBnDWGecbEucMyXjzxGv9cxsxsjS9u5s1w=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
@ -413,7 +416,7 @@ github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.15 h1:Y7xOFbD+3jaPw+VN7lkakNJ/pa+ZSQVFp1ONtJaBxns=
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.17 h1:1WuWJu7/e8SqK+uQl7lfk/N/oMZTL2NE/TJsNKRNMc4=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
@ -421,6 +424,7 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
@ -430,10 +434,10 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/urfave/cli/v2 v2.25.3 h1:VJkt6wvEBOoSjPFQvOkv6iWIrsJyCrKGtCtxXWwmGeY=
github.com/urfave/cli/v2 v2.25.3/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/urfave/cli/v2 v2.25.7 h1:VAzn5oq403l5pHjc4OhD54+XGO9cdKVL/7lDjF+iKUs=
github.com/urfave/cli/v2 v2.25.7/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.30.0/go.mod h1:2rsYD01CKFrjjsvFxx75KlEUNpWNBY9JWD3K/7o2Cus=
@ -465,14 +469,14 @@ go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.41.1 h1:pX+lppB8PArapyhS6nBStyQmkaDUPWdQf0UmEGRCQ54=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.41.1/go.mod h1:2FmkXne0k9nkp27LD/m+uoh8dNlstsiCJ7PLc/S72aI=
go.opentelemetry.io/otel v1.15.1 h1:3Iwq3lfRByPaws0f6bU3naAqOR1n5IeDWd9390kWHa8=
go.opentelemetry.io/otel v1.15.1/go.mod h1:mHHGEHVDLal6YrKMmk9LqC4a3sF5g+fHfrttQIB1NTc=
go.opentelemetry.io/otel/metric v0.38.1 h1:2MM7m6wPw9B8Qv8iHygoAgkbejed59uUR6ezR5T3X2s=
go.opentelemetry.io/otel/metric v0.38.1/go.mod h1:FwqNHD3I/5iX9pfrRGZIlYICrJv0rHEUl2Ln5vdIVnQ=
go.opentelemetry.io/otel/trace v1.15.1 h1:uXLo6iHJEzDfrNC0L0mNjItIp06SyaBQxu5t3xMlngY=
go.opentelemetry.io/otel/trace v1.15.1/go.mod h1:IWdQG/5N1x7f6YUlmdLeJvH9yxtuJAfc4VW5Agv9r/8=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.42.0 h1:pginetY7+onl4qN1vl0xW/V/v6OBZ0vVdH+esuJgvmM=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.42.0/go.mod h1:XiYsayHc36K3EByOO6nbAXnAWbrUxdjUROCEeeROOH8=
go.opentelemetry.io/otel v1.16.0 h1:Z7GVAX/UkAXPKsy94IU+i6thsQS4nb7LviLpnaNeW8s=
go.opentelemetry.io/otel v1.16.0/go.mod h1:vl0h9NUa1D5s1nv3A5vZOYWn8av4K8Ml6JDeHrT/bx4=
go.opentelemetry.io/otel/metric v1.16.0 h1:RbrpwVG1Hfv85LgnZ7+txXioPDoh6EdbZHo26Q3hqOo=
go.opentelemetry.io/otel/metric v1.16.0/go.mod h1:QE47cpOmkwipPiefDwo2wDzwJrlfxxNYodqc4xnGCo4=
go.opentelemetry.io/otel/trace v1.16.0 h1:8JRpaObFoW0pxuVPapkgH8UhHQj+bJW8jJsCZEu5MQs=
go.opentelemetry.io/otel/trace v1.16.0/go.mod h1:Yt9vYq1SdNz3xdjZZK7wcXv1qv2pwLkqr2QVwea0ef0=
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
@ -487,8 +491,8 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220314234659-1baeb1ce4c0b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g=
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA=
golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
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=
@ -499,8 +503,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc h1:mCRnTeVUjcrhlRmO0VK8a6k6Rrf6TF9htwo2pJVSjIU=
golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df h1:UA2aFVmmsIlefxMk29Dp2juaUSth8Pyn3Tq5Y5mJGME=
golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@ -522,7 +526,7 @@ golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzB
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/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk=
golang.org/x/mod v0.11.0 h1:bUO06HqtnRcc/7l71XBe4WcqTZ+3AH1J59zWDDwLKgU=
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-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@ -559,16 +563,16 @@ golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qx
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50=
golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
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=
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.8.0 h1:6dkIjl3j3LtZ/O3sTgZTMsLKSftL/B8Zgq4huOIIUu8=
golang.org/x/oauth2 v0.8.0/go.mod h1:yr7u4HXZRm1R1kBWqr/xKNqewf0plRYoB7sla+BCIXE=
golang.org/x/oauth2 v0.10.0 h1:zHCpF2Khkwy4mMB4bv0U37YtJdTGW8jI0glAApi0Kh8=
golang.org/x/oauth2 v0.10.0/go.mod h1:kTpgurOux7LqtuxjuyZa4Gj2gdezIt/jQtGnNFfypQI=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -580,8 +584,8 @@ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.2.0 h1:PUR+T4wwASmuSTYdKjYHI5TD22Wy5ogLU5qZCOLxBrI=
golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@ -620,18 +624,19 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.8.0 h1:n5xxQn2i3PC0yLAbjTpNT85q/Kgzcr2gIoX9OrJUols=
golang.org/x/term v0.10.0 h1:3R7pNqamzBraeqj/Tj8qt1aQ2HpmlC+Cx/qL/7hn4/c=
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=
@ -641,8 +646,8 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4=
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
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=
@ -691,7 +696,7 @@ golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.8.0 h1:vSDcovVPld282ceKgDimkRSC8kpaH1dgyc9UMzlt84Y=
golang.org/x/tools v0.9.3 h1:Gn1I8+64MsuTb/HpH+LmQtNas23LhUVr3rYZ0eKuaMM=
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/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@ -714,8 +719,8 @@ google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0M
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
google.golang.org/api v0.123.0 h1:yHVU//vA+qkOhm4reEC9LtzHVUCN/IqqNRl1iQ9xE20=
google.golang.org/api v0.123.0/go.mod h1:gcitW0lvnyWjSp9nKxAbdHKIZ6vF4aajGueeslZOyms=
google.golang.org/api v0.130.0 h1:A50ujooa1h9iizvfzA4rrJr2B7uRmWexwbekQ2+5FPQ=
google.golang.org/api v0.130.0/go.mod h1:J/LCJMYSDFvAVREGCbrESb53n4++NMBDetSHGL5I5RY=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
@ -754,8 +759,12 @@ google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7Fc
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 h1:KpwkzHKEF7B9Zxg18WzOa7djJ+Ha5DzthMyZYQfEn2A=
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU=
google.golang.org/genproto v0.0.0-20230706204954-ccb25ca9f130 h1:Au6te5hbKUV8pIYWHqOUZ1pva5qK/rwbIhoXEUB9Lu8=
google.golang.org/genproto v0.0.0-20230706204954-ccb25ca9f130/go.mod h1:O9kGHb51iE/nOGvQaDUuadVYqovW56s5emA88lQnj6Y=
google.golang.org/genproto/googleapis/api v0.0.0-20230706204954-ccb25ca9f130 h1:XVeBY8d/FaK4848myy41HBqnDwvxeV3zMZhwN1TvAMU=
google.golang.org/genproto/googleapis/api v0.0.0-20230706204954-ccb25ca9f130/go.mod h1:mPBs5jNgx2GuQGvFwUvVKqtn6HsUw9nP64BedgvqEsQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20230706204954-ccb25ca9f130 h1:2FZP5XuJY9zQyGM5N0rtovnoXjiMUEIUMvw0m9wlpLc=
google.golang.org/genproto/googleapis/rpc v0.0.0-20230706204954-ccb25ca9f130/go.mod h1:8mL13HKkDa+IuJ8yruA3ci0q+0vsUz4m//+ottjwS5o=
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=
@ -772,8 +781,8 @@ google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTp
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ=
google.golang.org/grpc v1.55.0 h1:3Oj82/tFSCeUrRTg/5E/7d/W5A1tj6Ky1ABAuZuv5ag=
google.golang.org/grpc v1.55.0/go.mod h1:iYEXKGkEBhg1PjZQvoYEVPTDkHo1/bjTnfwTeGONTY8=
google.golang.org/grpc v1.56.2 h1:fVRFRnXvU+x6C4IlHZewvJOVHoOv1TUuQyoRsYnB4bI=
google.golang.org/grpc v1.56.2/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
@ -786,8 +795,8 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@ -819,8 +828,8 @@ k8s.io/api v0.26.2 h1:dM3cinp3PGB6asOySalOZxEG4CZ0IAdJsrYZXE/ovGQ=
k8s.io/apimachinery v0.26.2 h1:da1u3D5wfR5u2RpLhE/ZtZS2P7QvDgLZTi9wrNZl/tQ=
k8s.io/client-go v0.26.2 h1:s1WkVujHX3kTp4Zn4yGNFK+dlDXy1bAAkIl+cFAiuYI=
k8s.io/klog v1.0.0 h1:Pt+yjF5aB1xDSVbau4VsWe+dQNzA0qv1LlXdC2dF6Q8=
k8s.io/klog/v2 v2.90.1 h1:m4bYOKall2MmOiRaR1J+We67Do7vm9KiQVlT96lnHUw=
k8s.io/kube-openapi v0.0.0-20230303024457-afdc3dddf62d h1:VcFq5n7wCJB2FQMCIHfC+f+jNcGgNMar1uKd6rVlifU=
k8s.io/klog/v2 v2.100.1 h1:7WCHKK6K8fNhTqfBhISHQ97KrnJNFZMcQvKp7gP/tmg=
k8s.io/kube-openapi v0.0.0-20230501164219-8b0f38b5fd1f h1:2kWPakN3i/k81b0gvD5C5FJ2kxm1WrQFanWchyKuqGg=
k8s.io/utils v0.0.0-20230308161112-d77c459e9343 h1:m7tbIjXGcGIAtpmQr7/NAi7RsWoW3E7Zcm4jI1HicTc=
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=

View file

@ -15,4 +15,4 @@
package internal
// Version is the current tagged release of the library.
const Version = "1.19.3"
const Version = "1.20.1"

View file

@ -1,6 +1,20 @@
# Changes
## [1.1.1](https://github.com/googleapis/google-cloud-go/compare/iam/v1.1.0...iam/v1.1.1) (2023-06-20)
### Bug Fixes
* **iam:** REST query UpdateMask bug ([df52820](https://github.com/googleapis/google-cloud-go/commit/df52820b0e7721954809a8aa8700b93c5662dc9b))
## [1.1.0](https://github.com/googleapis/google-cloud-go/compare/iam/v1.0.1...iam/v1.1.0) (2023-05-30)
### Features
* **iam:** Update all direct dependencies ([b340d03](https://github.com/googleapis/google-cloud-go/commit/b340d030f2b52a4ce48846ce63984b28583abde6))
## [1.0.1](https://github.com/googleapis/google-cloud-go/compare/iam/v1.0.0...iam/v1.0.1) (2023-05-08)

View file

@ -15,7 +15,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.30.0
// protoc v3.21.12
// protoc v4.23.2
// source: google/iam/v1/iam_policy.proto
package iampb

View file

@ -15,7 +15,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.30.0
// protoc v3.21.12
// protoc v4.23.2
// source: google/iam/v1/options.proto
package iampb

View file

@ -15,7 +15,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.30.0
// protoc v3.21.12
// protoc v4.23.2
// source: google/iam/v1/policy.proto
package iampb

File diff suppressed because it is too large Load diff

View file

@ -17,21 +17,6 @@ tools would then talk to pkg.go.dev or some other service to get the overall
list of packages and use the `.repo-metadata.json` files to get the additional
metadata required. For now, `.repo-metadata-full.json` includes everything.
## cloudbuild.yaml
The `cloudbuild.yaml` Cloud Build configuration currently supports:
* Building a docker container from the `internal/postprocessor/Dockerfile`.
The build can be run locally in the `google-cloud-go` root directory:
```bash
gcloud builds submit --project=cloud-devrel-kokoro-resources --config=internal/cloudbuild.yaml
```
See the [postprocessor/README](postprocessor/README.md) for instructions
regarding updating the post-processor docker container.
### Updating OwlBot SHA
You may want to manually update the which version of the post-processor will be

View file

@ -1,25 +0,0 @@
# Copyright 2023 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# note: /workspace is a special directory in the docker image where all the files in this folder
# get placed on your behalf
timeout: 7200s # 2 hours
steps:
- name: gcr.io/cloud-builders/docker
args: ['build', '-t', 'gcr.io/cloud-devrel-public-resources/owlbot-go', '-f', 'postprocessor/Dockerfile', '.']
dir: internal
images:
- gcr.io/cloud-devrel-public-resources/owlbot-go:latest

View file

@ -20,7 +20,6 @@ import (
"time"
gax "github.com/googleapis/gax-go/v2"
"google.golang.org/grpc/status"
)
// Retry calls the supplied function f repeatedly according to the provided
@ -75,11 +74,3 @@ func (e wrappedCallErr) Unwrap() error {
func (e wrappedCallErr) Is(err error) bool {
return e.ctxErr == err || e.wrappedErr == err
}
// GRPCStatus allows the wrapped error to be used with status.FromError.
func (e wrappedCallErr) GRPCStatus() *status.Status {
if s, ok := status.FromError(e.wrappedErr); ok {
return s
}
return nil
}

View file

@ -1,6 +1,35 @@
# Changes
## [1.31.0](https://github.com/googleapis/google-cloud-go/compare/storage/v1.30.1...storage/v1.31.0) (2023-06-27)
### Features
* **storage/internal:** Add ctype=CORD for ChecksummedData.content ([ca94e27](https://github.com/googleapis/google-cloud-go/commit/ca94e2724f9e2610b46aefd0a3b5ddc06102e91b))
* **storage:** Add support for MatchGlob ([#8097](https://github.com/googleapis/google-cloud-go/issues/8097)) ([9426a5a](https://github.com/googleapis/google-cloud-go/commit/9426a5a45d4c2fd07f84261f6d602680e79cdc48)), refs [#7727](https://github.com/googleapis/google-cloud-go/issues/7727) [#7728](https://github.com/googleapis/google-cloud-go/issues/7728)
* **storage:** Respect WithEndpoint for SignedURLs and PostPolicy ([#8113](https://github.com/googleapis/google-cloud-go/issues/8113)) ([f918f23](https://github.com/googleapis/google-cloud-go/commit/f918f23a3cda4fbc8d709e32b914ead8b735d664))
* **storage:** Update all direct dependencies ([b340d03](https://github.com/googleapis/google-cloud-go/commit/b340d030f2b52a4ce48846ce63984b28583abde6))
### Bug Fixes
* **storage:** Fix CreateBucket logic for gRPC ([#8165](https://github.com/googleapis/google-cloud-go/issues/8165)) ([8424e7e](https://github.com/googleapis/google-cloud-go/commit/8424e7e145a117c91006318fa924a8b2643c1c7e)), refs [#8162](https://github.com/googleapis/google-cloud-go/issues/8162)
* **storage:** Fix reads with "./" in object names [XML] ([#8017](https://github.com/googleapis/google-cloud-go/issues/8017)) ([6b7b21f](https://github.com/googleapis/google-cloud-go/commit/6b7b21f8a334b6ad3a25e1f66ae1265b4d1f0995))
* **storage:** Fix routing header for writes ([#8159](https://github.com/googleapis/google-cloud-go/issues/8159)) ([42a59f5](https://github.com/googleapis/google-cloud-go/commit/42a59f5a23ab9b4743ab032ad92304922c801d93)), refs [#8142](https://github.com/googleapis/google-cloud-go/issues/8142) [#8143](https://github.com/googleapis/google-cloud-go/issues/8143) [#8144](https://github.com/googleapis/google-cloud-go/issues/8144) [#8145](https://github.com/googleapis/google-cloud-go/issues/8145) [#8149](https://github.com/googleapis/google-cloud-go/issues/8149)
* **storage:** REST query UpdateMask bug ([df52820](https://github.com/googleapis/google-cloud-go/commit/df52820b0e7721954809a8aa8700b93c5662dc9b))
* **storage:** Update grpc to v1.55.0 ([1147ce0](https://github.com/googleapis/google-cloud-go/commit/1147ce02a990276ca4f8ab7a1ab65c14da4450ef))
### Documentation
* **storage/internal:** Clarifications about behavior of DeleteObject RPC ([3f1ed9c](https://github.com/googleapis/google-cloud-go/commit/3f1ed9c63fb115f47607a3ab478842fe5ba0df11))
* **storage/internal:** Clarified the behavior of supplying bucket.name field in CreateBucket to reflect actual implementation ([ebae64d](https://github.com/googleapis/google-cloud-go/commit/ebae64d53397ec5dfe851f098754eaa1f5df7cb1))
* **storage/internal:** Revert ChecksummedData message definition not to specify ctype=CORD, because it would be a breaking change. ([ef61e47](https://github.com/googleapis/google-cloud-go/commit/ef61e4799280a355b960da8ae240ceb2efbe71ac))
* **storage/internal:** Update routing annotations for CancelResumableWriteRequest and QueryWriteStatusRequest ([4900851](https://github.com/googleapis/google-cloud-go/commit/49008518e168fe6f7891b907d6fc14eecdef758c))
* **storage/internal:** Updated ChecksummedData message definition to specify ctype=CORD, and removed incorrect earlier attempt that set that annotation in the ReadObjectResponse message definition ([ef61e47](https://github.com/googleapis/google-cloud-go/commit/ef61e4799280a355b960da8ae240ceb2efbe71ac))
* **storage:** WithXMLReads should mention XML instead of JSON API ([#7881](https://github.com/googleapis/google-cloud-go/issues/7881)) ([36f56c8](https://github.com/googleapis/google-cloud-go/commit/36f56c80c456ca74ffc03df76844ce15980ced82))
## [1.30.1](https://github.com/googleapis/google-cloud-go/compare/storage/v1.30.0...storage/v1.30.1) (2023-03-21)

View file

@ -20,7 +20,7 @@ import (
"reflect"
"cloud.google.com/go/internal/trace"
storagepb "cloud.google.com/go/storage/internal/apiv2/stubs"
"cloud.google.com/go/storage/internal/apiv2/storagepb"
raw "google.golang.org/api/storage/v1"
)

View file

@ -27,7 +27,7 @@ import (
"cloud.google.com/go/compute/metadata"
"cloud.google.com/go/internal/optional"
"cloud.google.com/go/internal/trace"
storagepb "cloud.google.com/go/storage/internal/apiv2/stubs"
"cloud.google.com/go/storage/internal/apiv2/storagepb"
"google.golang.org/api/googleapi"
"google.golang.org/api/iamcredentials/v1"
"google.golang.org/api/iterator"
@ -173,12 +173,18 @@ func (b *BucketHandle) Update(ctx context.Context, uattrs BucketAttrsToUpdate) (
// [Overview of access control]: https://cloud.google.com/storage/docs/accesscontrol#signed_urls_query_string_authentication
// [automatic detection of credentials]: https://pkg.go.dev/cloud.google.com/go/storage#hdr-Credential_requirements_for_signing
func (b *BucketHandle) SignedURL(object string, opts *SignedURLOptions) (string, error) {
if opts.GoogleAccessID != "" && (opts.SignBytes != nil || len(opts.PrivateKey) > 0) {
return SignedURL(b.name, object, opts)
}
// Make a copy of opts so we don't modify the pointer parameter.
newopts := opts.clone()
if newopts.Hostname == "" {
// Extract the correct host from the readhost set on the client
newopts.Hostname = b.c.xmlHost
}
if opts.GoogleAccessID != "" && (opts.SignBytes != nil || len(opts.PrivateKey) > 0) {
return SignedURL(b.name, object, newopts)
}
if newopts.GoogleAccessID == "" {
id, err := b.detectDefaultGoogleAccessID()
if err != nil {
@ -215,12 +221,18 @@ func (b *BucketHandle) SignedURL(object string, opts *SignedURLOptions) (string,
//
// [automatic detection of credentials]: https://pkg.go.dev/cloud.google.com/go/storage#hdr-Credential_requirements_for_signing
func (b *BucketHandle) GenerateSignedPostPolicyV4(object string, opts *PostPolicyV4Options) (*PostPolicyV4, error) {
if opts.GoogleAccessID != "" && (opts.SignRawBytes != nil || opts.SignBytes != nil || len(opts.PrivateKey) > 0) {
return GenerateSignedPostPolicyV4(b.name, object, opts)
}
// Make a copy of opts so we don't modify the pointer parameter.
newopts := opts.clone()
if newopts.Hostname == "" {
// Extract the correct host from the readhost set on the client
newopts.Hostname = b.c.xmlHost
}
if opts.GoogleAccessID != "" && (opts.SignRawBytes != nil || opts.SignBytes != nil || len(opts.PrivateKey) > 0) {
return GenerateSignedPostPolicyV4(b.name, object, newopts)
}
if newopts.GoogleAccessID == "" {
id, err := b.detectDefaultGoogleAccessID()
if err != nil {
@ -921,8 +933,6 @@ func (ua *BucketAttrsToUpdate) toProtoBucket() *storagepb.Bucket {
return &storagepb.Bucket{}
}
// TODO(cathyo): Handle labels. Pending b/230510191.
var v *storagepb.Bucket_Versioning
if ua.VersioningEnabled != nil {
v = &storagepb.Bucket_Versioning{Enabled: optional.ToBool(ua.VersioningEnabled)}
@ -996,6 +1006,7 @@ func (ua *BucketAttrsToUpdate) toProtoBucket() *storagepb.Bucket {
IamConfig: bktIAM,
Rpo: ua.RPO.String(),
Autoclass: ua.Autoclass.toProtoAutoclass(),
Labels: ua.setLabels,
}
}
@ -1264,7 +1275,9 @@ func (ua *BucketAttrsToUpdate) toRawBucket() *raw.Bucket {
}
// If returns a new BucketHandle that applies a set of preconditions.
// Preconditions already set on the BucketHandle are ignored.
// Preconditions already set on the BucketHandle are ignored. The supplied
// BucketConditions must have exactly one field set to a non-zero value;
// otherwise an error will be returned from any operation on the BucketHandle.
// Operations on the new handle will return an error if the preconditions are not
// satisfied. The only valid preconditions for buckets are MetagenerationMatch
// and MetagenerationNotMatch.

View file

@ -26,8 +26,9 @@ import (
"cloud.google.com/go/iam/apiv1/iampb"
"cloud.google.com/go/internal/trace"
gapic "cloud.google.com/go/storage/internal/apiv2"
storagepb "cloud.google.com/go/storage/internal/apiv2/stubs"
"cloud.google.com/go/storage/internal/apiv2/storagepb"
"github.com/googleapis/gax-go/v2"
"google.golang.org/api/googleapi"
"google.golang.org/api/iterator"
"google.golang.org/api/option"
"google.golang.org/api/option/internaloption"
@ -153,7 +154,7 @@ func (c *grpcStorageClient) GetServiceAccount(ctx context.Context, project strin
func (c *grpcStorageClient) CreateBucket(ctx context.Context, project, bucket string, attrs *BucketAttrs, opts ...storageOption) (*BucketAttrs, error) {
s := callSettings(c.settings, opts...)
b := attrs.toProtoBucket()
b.Name = bucket
b.Project = toProjectResource(project)
// If there is lifecycle information but no location, explicitly set
// the location. This is a GCS quirk/bug.
if b.GetLocation() == "" && b.GetLifecycle() != nil {
@ -161,9 +162,9 @@ func (c *grpcStorageClient) CreateBucket(ctx context.Context, project, bucket st
}
req := &storagepb.CreateBucketRequest{
Parent: toProjectResource(project),
Parent: fmt.Sprintf("projects/%s", globalProjectAlias),
Bucket: b,
BucketId: b.GetName(),
BucketId: bucket,
}
if attrs != nil {
req.PredefinedAcl = attrs.PredefinedACL
@ -354,7 +355,16 @@ func (c *grpcStorageClient) UpdateBucket(ctx context.Context, bucket string, uat
if uattrs.Autoclass != nil {
fieldMask.Paths = append(fieldMask.Paths, "autoclass")
}
// TODO(cathyo): Handle labels. Pending b/230510191.
for label := range uattrs.setLabels {
fieldMask.Paths = append(fieldMask.Paths, fmt.Sprintf("labels.%s", label))
}
// Delete a label by not including it in Bucket.Labels but adding the key to the update mask.
for label := range uattrs.deleteLabels {
fieldMask.Paths = append(fieldMask.Paths, fmt.Sprintf("labels.%s", label))
}
req.UpdateMask = fieldMask
var battrs *BucketAttrs
@ -404,6 +414,11 @@ func (c *grpcStorageClient) ListObjects(ctx context.Context, bucket string, q *Q
}
gitr := c.raw.ListObjects(it.ctx, req, s.gax...)
fetch := func(pageSize int, pageToken string) (token string, err error) {
// MatchGlob not yet supported for gRPC.
// TODO: add support when b/287306063 resolved.
if q != nil && q.MatchGlob != "" {
return "", status.Errorf(codes.Unimplemented, "MatchGlob is not supported for gRPC")
}
var objects []*storagepb.Object
err = run(it.ctx, func() error {
objects, token, err = gitr.InternalFetch(pageSize, pageToken)
@ -540,7 +555,19 @@ func (c *grpcStorageClient) UpdateObject(ctx context.Context, bucket, object str
if uattrs.ACL != nil || len(uattrs.PredefinedACL) > 0 {
fieldMask.Paths = append(fieldMask.Paths, "acl")
}
// TODO(cathyo): Handle metadata. Pending b/230510191.
if uattrs.Metadata != nil {
// We don't support deleting a specific metadata key; metadata is deleted
// as a whole if provided an empty map, so we do not use dot notation here
if len(uattrs.Metadata) == 0 {
fieldMask.Paths = append(fieldMask.Paths, "metadata")
} else {
// We can, however, use dot notation for adding keys
for key := range uattrs.Metadata {
fieldMask.Paths = append(fieldMask.Paths, fmt.Sprintf("metadata.%s", key))
}
}
}
req.UpdateMask = fieldMask
@ -885,15 +912,8 @@ func (c *grpcStorageClient) NewRangeReader(ctx context.Context, params *newRange
req.ReadOffset = params.offset + seen
// A negative length means "read to the end of the object", but the
// read_limit field it corresponds to uses zero to mean the same thing. Thus
// we coerce the length to 0 to read to the end of the object.
if params.length < 0 {
params.length = 0
}
// Only set a ReadLimit if length is greater than zero, because zero
// means read it all.
// Only set a ReadLimit if length is greater than zero, because <= 0 means
// to read it all.
if params.length > 0 {
req.ReadLimit = params.length - seen
}
@ -963,6 +983,7 @@ func (c *grpcStorageClient) NewRangeReader(ctx context.Context, params *newRange
// client buffer for reading later.
leftovers: msg.GetChecksummedData().GetContent(),
settings: s,
zeroRange: params.length == 0,
},
}
@ -974,8 +995,15 @@ func (c *grpcStorageClient) NewRangeReader(ctx context.Context, params *newRange
r.remain = size
}
// For a zero-length request, explicitly close the stream and set remaining
// bytes to zero.
if params.length == 0 {
r.remain = 0
r.reader.Close()
}
// Only support checksums when reading an entire object, not a range.
if checksums := msg.GetObjectChecksums(); checksums != nil && checksums.Crc32C != nil && params.offset == 0 && params.length == 0 {
if checksums := msg.GetObjectChecksums(); checksums != nil && checksums.Crc32C != nil && params.offset == 0 && params.length < 0 {
r.wantCRC = checksums.GetCrc32C()
r.checkCRC = true
}
@ -1036,11 +1064,13 @@ func (c *grpcStorageClient) OpenWriter(params *openWriterParams, opts ...storage
pr.CloseWithError(err)
return
}
// At this point, the current buffer has been uploaded. Capture the
// committed offset here in case the upload was not finalized and
// another chunk is to be uploaded.
offset = off
progress(offset)
// At this point, the current buffer has been uploaded. For resumable
// uploads, capture the committed offset here in case the upload was not
// finalized and another chunk is to be uploaded.
if gw.upid != "" {
offset = off
progress(offset)
}
// When we are done reading data and the chunk has been finalized,
// we are done.
@ -1335,6 +1365,7 @@ type readStreamResponse struct {
type gRPCReader struct {
seen, size int64
zeroRange bool
stream storagepb.Storage_ReadObjectClient
reopen func(seen int64) (*readStreamResponse, context.CancelFunc, error)
leftovers []byte
@ -1344,7 +1375,12 @@ type gRPCReader struct {
// Read reads bytes into the user's buffer from an open gRPC stream.
func (r *gRPCReader) Read(p []byte) (int, error) {
// No stream to read from, either never initiliazed or Close was called.
// The entire object has been read by this reader, return EOF.
if r.size == r.seen || r.zeroRange {
return 0, io.EOF
}
// No stream to read from, either never initialized or Close was called.
// Note: There is a potential concurrency issue if multiple routines are
// using the same reader. One encounters an error and the stream is closed
// and then reopened while the other routine attempts to read from it.
@ -1352,11 +1388,6 @@ func (r *gRPCReader) Read(p []byte) (int, error) {
return 0, fmt.Errorf("reader has been closed")
}
// The entire object has been read by this reader, return EOF.
if r.size != 0 && r.size == r.seen {
return 0, io.EOF
}
var n int
// Read leftovers and return what was available to conform to the Reader
// interface: https://pkg.go.dev/io#Reader.
@ -1447,6 +1478,12 @@ func (r *gRPCReader) reopenStream() (*storagepb.ReadObjectResponse, error) {
func newGRPCWriter(c *grpcStorageClient, params *openWriterParams, r io.Reader) *gRPCWriter {
size := params.chunkSize
// Round up chunksize to nearest 256KiB
if size%googleapi.MinUploadChunkSize != 0 {
size += googleapi.MinUploadChunkSize - (size % googleapi.MinUploadChunkSize)
}
if params.chunkSize == 0 {
// TODO: Should we actually use the minimum of 256 KB here when the user
// indicates they want minimal memory usage? We cannot do a zero-copy,
@ -1578,7 +1615,7 @@ func (w *gRPCWriter) uploadBuffer(recvd int, start int64, doneReading bool) (*st
// The first message on the WriteObject stream must either be the
// Object or the Resumable Upload ID.
if first {
ctx := gapic.InsertMetadata(w.ctx, metadata.Pairs("x-goog-request-params", "bucket="+url.QueryEscape(w.bucket)))
ctx := gapic.InsertMetadata(w.ctx, metadata.Pairs("x-goog-request-params", fmt.Sprintf("bucket=projects/_/buckets/%s", url.QueryEscape(w.bucket))))
w.stream, err = w.c.raw.WriteObject(ctx)
if err != nil {
return nil, 0, false, err

View file

@ -20,7 +20,7 @@ import (
"fmt"
"time"
storagepb "cloud.google.com/go/storage/internal/apiv2/stubs"
"cloud.google.com/go/storage/internal/apiv2/storagepb"
"google.golang.org/api/iterator"
raw "google.golang.org/api/storage/v1"
)

View file

@ -49,7 +49,7 @@ import (
type httpStorageClient struct {
creds *google.Credentials
hc *http.Client
readHost string
xmlHost string
raw *raw.Service
scheme string
settings *settings
@ -123,7 +123,7 @@ func newHTTPStorageClient(ctx context.Context, opts ...storageOption) (storageCl
if err != nil {
return nil, fmt.Errorf("storage client: %w", err)
}
// Update readHost and scheme with the chosen endpoint.
// Update xmlHost and scheme with the chosen endpoint.
u, err := url.Parse(ep)
if err != nil {
return nil, fmt.Errorf("supplied endpoint %q is not valid: %w", ep, err)
@ -132,7 +132,7 @@ func newHTTPStorageClient(ctx context.Context, opts ...storageOption) (storageCl
return &httpStorageClient{
creds: creds,
hc: hc,
readHost: u.Host,
xmlHost: u.Host,
raw: rawService,
scheme: u.Scheme,
settings: s,
@ -347,6 +347,7 @@ func (c *httpStorageClient) ListObjects(ctx context.Context, bucket string, q *Q
req.EndOffset(it.query.EndOffset)
req.Versions(it.query.Versions)
req.IncludeTrailingDelimiter(it.query.IncludeTrailingDelimiter)
req.MatchGlob(it.query.MatchGlob)
if selection := it.query.toFieldSelection(); selection != "" {
req.Fields("nextPageToken", googleapi.Field(selection))
}
@ -790,9 +791,10 @@ func (c *httpStorageClient) NewRangeReader(ctx context.Context, params *newRange
func (c *httpStorageClient) newRangeReaderXML(ctx context.Context, params *newRangeReaderParams, s *settings) (r *Reader, err error) {
u := &url.URL{
Scheme: c.scheme,
Host: c.readHost,
Path: fmt.Sprintf("/%s/%s", params.bucket, params.object),
Scheme: c.scheme,
Host: c.xmlHost,
Path: fmt.Sprintf("/%s/%s", params.bucket, params.object),
RawPath: fmt.Sprintf("/%s/%s", params.bucket, url.PathEscape(params.object)),
}
verb := "GET"
if params.length == 0 {
@ -1373,6 +1375,8 @@ func parseReadResponse(res *http.Response, params *newRangeReaderParams, reopen
remain := res.ContentLength
body := res.Body
// If the user requested zero bytes, explicitly close and remove the request
// body.
if params.length == 0 {
remain = 0
body.Close()

View file

@ -19,8 +19,6 @@
//
// Lets you store and retrieve potentially-large, immutable data objects.
//
// NOTE: This package is in alpha. It is not stable, and is likely to change.
//
// # General documentation
//
// For information about setting deadlines, reusing contexts, and more
@ -64,13 +62,18 @@
//
// req := &storagepb.DeleteBucketRequest{
// // TODO: Fill request struct fields.
// // See https://pkg.go.dev/cloud.google.com/go/storage/internal/apiv2/stubs#DeleteBucketRequest.
// // See https://pkg.go.dev/cloud.google.com/go/storage/internal/apiv2/storagepb#DeleteBucketRequest.
// }
// err = c.DeleteBucket(ctx, req)
// if err != nil {
// // TODO: Handle error.
// }
//
// # Inspecting errors
//
// To see examples of how to inspect errors returned by this package please reference
// [Inspecting errors](https://pkg.go.dev/cloud.google.com/go#hdr-Inspecting_errors).
//
// # Use of Context
//
// The ctx passed to NewClient is used for authentication requests and
@ -82,11 +85,6 @@ package storage // import "cloud.google.com/go/storage/internal/apiv2"
import (
"context"
"os"
"runtime"
"strconv"
"strings"
"unicode"
"google.golang.org/api/option"
"google.golang.org/grpc/metadata"
@ -117,16 +115,6 @@ func insertMetadata(ctx context.Context, mds ...metadata.MD) context.Context {
return metadata.NewOutgoingContext(ctx, out)
}
func checkDisableDeadlines() (bool, error) {
raw, ok := os.LookupEnv("GOOGLE_API_GO_EXPERIMENTAL_DISABLE_DEFAULT_DEADLINE")
if !ok {
return false, nil
}
b, err := strconv.ParseBool(raw)
return b, err
}
// DefaultAuthScopes reports the default set of authentication scopes to use with this package.
func DefaultAuthScopes() []string {
return []string{
@ -137,40 +125,3 @@ func DefaultAuthScopes() []string {
"https://www.googleapis.com/auth/devstorage.read_write",
}
}
// versionGo returns the Go runtime version. The returned string
// has no whitespace, suitable for reporting in header.
func versionGo() string {
const develPrefix = "devel +"
s := runtime.Version()
if strings.HasPrefix(s, develPrefix) {
s = s[len(develPrefix):]
if p := strings.IndexFunc(s, unicode.IsSpace); p >= 0 {
s = s[:p]
}
return s
}
notSemverRune := func(r rune) bool {
return !strings.ContainsRune("0123456789.", r)
}
if strings.HasPrefix(s, "go1") {
s = s[2:]
var prerelease string
if p := strings.IndexFunc(s, notSemverRune); p >= 0 {
s, prerelease = s[:p], s[p:]
}
if strings.HasSuffix(s, ".") {
s += "0"
} else if strings.Count(s, ".") < 2 {
s += ".0"
}
if prerelease != "" {
s += "-" + prerelease
}
return s
}
return "UNKNOWN"
}

View file

@ -23,15 +23,17 @@ import (
"net/url"
"regexp"
"strings"
"time"
iampb "cloud.google.com/go/iam/apiv1/iampb"
storagepb "cloud.google.com/go/storage/internal/apiv2/stubs"
storagepb "cloud.google.com/go/storage/internal/apiv2/storagepb"
gax "github.com/googleapis/gax-go/v2"
"google.golang.org/api/iterator"
"google.golang.org/api/option"
"google.golang.org/api/option/internaloption"
gtransport "google.golang.org/api/transport/grpc"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/metadata"
"google.golang.org/protobuf/proto"
)
@ -86,36 +88,394 @@ func defaultGRPCClientOptions() []option.ClientOption {
func defaultCallOptions() *CallOptions {
return &CallOptions{
DeleteBucket: []gax.CallOption{},
GetBucket: []gax.CallOption{},
CreateBucket: []gax.CallOption{},
ListBuckets: []gax.CallOption{},
LockBucketRetentionPolicy: []gax.CallOption{},
GetIamPolicy: []gax.CallOption{},
SetIamPolicy: []gax.CallOption{},
TestIamPermissions: []gax.CallOption{},
UpdateBucket: []gax.CallOption{},
DeleteNotificationConfig: []gax.CallOption{},
GetNotificationConfig: []gax.CallOption{},
CreateNotificationConfig: []gax.CallOption{},
ListNotificationConfigs: []gax.CallOption{},
ComposeObject: []gax.CallOption{},
DeleteObject: []gax.CallOption{},
CancelResumableWrite: []gax.CallOption{},
GetObject: []gax.CallOption{},
ReadObject: []gax.CallOption{},
UpdateObject: []gax.CallOption{},
WriteObject: []gax.CallOption{},
ListObjects: []gax.CallOption{},
RewriteObject: []gax.CallOption{},
StartResumableWrite: []gax.CallOption{},
QueryWriteStatus: []gax.CallOption{},
GetServiceAccount: []gax.CallOption{},
CreateHmacKey: []gax.CallOption{},
DeleteHmacKey: []gax.CallOption{},
GetHmacKey: []gax.CallOption{},
ListHmacKeys: []gax.CallOption{},
UpdateHmacKey: []gax.CallOption{},
DeleteBucket: []gax.CallOption{
gax.WithTimeout(60000 * time.Millisecond),
gax.WithRetry(func() gax.Retryer {
return gax.OnCodes([]codes.Code{
codes.DeadlineExceeded,
codes.Unavailable,
}, gax.Backoff{
Initial: 1000 * time.Millisecond,
Max: 60000 * time.Millisecond,
Multiplier: 2.00,
})
}),
},
GetBucket: []gax.CallOption{
gax.WithTimeout(60000 * time.Millisecond),
gax.WithRetry(func() gax.Retryer {
return gax.OnCodes([]codes.Code{
codes.DeadlineExceeded,
codes.Unavailable,
}, gax.Backoff{
Initial: 1000 * time.Millisecond,
Max: 60000 * time.Millisecond,
Multiplier: 2.00,
})
}),
},
CreateBucket: []gax.CallOption{
gax.WithTimeout(60000 * time.Millisecond),
gax.WithRetry(func() gax.Retryer {
return gax.OnCodes([]codes.Code{
codes.DeadlineExceeded,
codes.Unavailable,
}, gax.Backoff{
Initial: 1000 * time.Millisecond,
Max: 60000 * time.Millisecond,
Multiplier: 2.00,
})
}),
},
ListBuckets: []gax.CallOption{
gax.WithTimeout(60000 * time.Millisecond),
gax.WithRetry(func() gax.Retryer {
return gax.OnCodes([]codes.Code{
codes.DeadlineExceeded,
codes.Unavailable,
}, gax.Backoff{
Initial: 1000 * time.Millisecond,
Max: 60000 * time.Millisecond,
Multiplier: 2.00,
})
}),
},
LockBucketRetentionPolicy: []gax.CallOption{
gax.WithTimeout(60000 * time.Millisecond),
gax.WithRetry(func() gax.Retryer {
return gax.OnCodes([]codes.Code{
codes.DeadlineExceeded,
codes.Unavailable,
}, gax.Backoff{
Initial: 1000 * time.Millisecond,
Max: 60000 * time.Millisecond,
Multiplier: 2.00,
})
}),
},
GetIamPolicy: []gax.CallOption{
gax.WithTimeout(60000 * time.Millisecond),
gax.WithRetry(func() gax.Retryer {
return gax.OnCodes([]codes.Code{
codes.DeadlineExceeded,
codes.Unavailable,
}, gax.Backoff{
Initial: 1000 * time.Millisecond,
Max: 60000 * time.Millisecond,
Multiplier: 2.00,
})
}),
},
SetIamPolicy: []gax.CallOption{
gax.WithTimeout(60000 * time.Millisecond),
gax.WithRetry(func() gax.Retryer {
return gax.OnCodes([]codes.Code{
codes.DeadlineExceeded,
codes.Unavailable,
}, gax.Backoff{
Initial: 1000 * time.Millisecond,
Max: 60000 * time.Millisecond,
Multiplier: 2.00,
})
}),
},
TestIamPermissions: []gax.CallOption{
gax.WithTimeout(60000 * time.Millisecond),
gax.WithRetry(func() gax.Retryer {
return gax.OnCodes([]codes.Code{
codes.DeadlineExceeded,
codes.Unavailable,
}, gax.Backoff{
Initial: 1000 * time.Millisecond,
Max: 60000 * time.Millisecond,
Multiplier: 2.00,
})
}),
},
UpdateBucket: []gax.CallOption{
gax.WithTimeout(60000 * time.Millisecond),
gax.WithRetry(func() gax.Retryer {
return gax.OnCodes([]codes.Code{
codes.DeadlineExceeded,
codes.Unavailable,
}, gax.Backoff{
Initial: 1000 * time.Millisecond,
Max: 60000 * time.Millisecond,
Multiplier: 2.00,
})
}),
},
DeleteNotificationConfig: []gax.CallOption{
gax.WithTimeout(60000 * time.Millisecond),
gax.WithRetry(func() gax.Retryer {
return gax.OnCodes([]codes.Code{
codes.DeadlineExceeded,
codes.Unavailable,
}, gax.Backoff{
Initial: 1000 * time.Millisecond,
Max: 60000 * time.Millisecond,
Multiplier: 2.00,
})
}),
},
GetNotificationConfig: []gax.CallOption{
gax.WithTimeout(60000 * time.Millisecond),
gax.WithRetry(func() gax.Retryer {
return gax.OnCodes([]codes.Code{
codes.DeadlineExceeded,
codes.Unavailable,
}, gax.Backoff{
Initial: 1000 * time.Millisecond,
Max: 60000 * time.Millisecond,
Multiplier: 2.00,
})
}),
},
CreateNotificationConfig: []gax.CallOption{
gax.WithTimeout(60000 * time.Millisecond),
gax.WithRetry(func() gax.Retryer {
return gax.OnCodes([]codes.Code{
codes.DeadlineExceeded,
codes.Unavailable,
}, gax.Backoff{
Initial: 1000 * time.Millisecond,
Max: 60000 * time.Millisecond,
Multiplier: 2.00,
})
}),
},
ListNotificationConfigs: []gax.CallOption{
gax.WithTimeout(60000 * time.Millisecond),
gax.WithRetry(func() gax.Retryer {
return gax.OnCodes([]codes.Code{
codes.DeadlineExceeded,
codes.Unavailable,
}, gax.Backoff{
Initial: 1000 * time.Millisecond,
Max: 60000 * time.Millisecond,
Multiplier: 2.00,
})
}),
},
ComposeObject: []gax.CallOption{
gax.WithTimeout(60000 * time.Millisecond),
gax.WithRetry(func() gax.Retryer {
return gax.OnCodes([]codes.Code{
codes.DeadlineExceeded,
codes.Unavailable,
}, gax.Backoff{
Initial: 1000 * time.Millisecond,
Max: 60000 * time.Millisecond,
Multiplier: 2.00,
})
}),
},
DeleteObject: []gax.CallOption{
gax.WithTimeout(60000 * time.Millisecond),
gax.WithRetry(func() gax.Retryer {
return gax.OnCodes([]codes.Code{
codes.DeadlineExceeded,
codes.Unavailable,
}, gax.Backoff{
Initial: 1000 * time.Millisecond,
Max: 60000 * time.Millisecond,
Multiplier: 2.00,
})
}),
},
CancelResumableWrite: []gax.CallOption{
gax.WithTimeout(60000 * time.Millisecond),
gax.WithRetry(func() gax.Retryer {
return gax.OnCodes([]codes.Code{
codes.DeadlineExceeded,
codes.Unavailable,
}, gax.Backoff{
Initial: 1000 * time.Millisecond,
Max: 60000 * time.Millisecond,
Multiplier: 2.00,
})
}),
},
GetObject: []gax.CallOption{
gax.WithTimeout(60000 * time.Millisecond),
gax.WithRetry(func() gax.Retryer {
return gax.OnCodes([]codes.Code{
codes.DeadlineExceeded,
codes.Unavailable,
}, gax.Backoff{
Initial: 1000 * time.Millisecond,
Max: 60000 * time.Millisecond,
Multiplier: 2.00,
})
}),
},
ReadObject: []gax.CallOption{
gax.WithRetry(func() gax.Retryer {
return gax.OnCodes([]codes.Code{
codes.DeadlineExceeded,
codes.Unavailable,
}, gax.Backoff{
Initial: 1000 * time.Millisecond,
Max: 60000 * time.Millisecond,
Multiplier: 2.00,
})
}),
},
UpdateObject: []gax.CallOption{
gax.WithTimeout(60000 * time.Millisecond),
gax.WithRetry(func() gax.Retryer {
return gax.OnCodes([]codes.Code{
codes.DeadlineExceeded,
codes.Unavailable,
}, gax.Backoff{
Initial: 1000 * time.Millisecond,
Max: 60000 * time.Millisecond,
Multiplier: 2.00,
})
}),
},
WriteObject: []gax.CallOption{
gax.WithRetry(func() gax.Retryer {
return gax.OnCodes([]codes.Code{
codes.DeadlineExceeded,
codes.Unavailable,
}, gax.Backoff{
Initial: 1000 * time.Millisecond,
Max: 60000 * time.Millisecond,
Multiplier: 2.00,
})
}),
},
ListObjects: []gax.CallOption{
gax.WithTimeout(60000 * time.Millisecond),
gax.WithRetry(func() gax.Retryer {
return gax.OnCodes([]codes.Code{
codes.DeadlineExceeded,
codes.Unavailable,
}, gax.Backoff{
Initial: 1000 * time.Millisecond,
Max: 60000 * time.Millisecond,
Multiplier: 2.00,
})
}),
},
RewriteObject: []gax.CallOption{
gax.WithTimeout(60000 * time.Millisecond),
gax.WithRetry(func() gax.Retryer {
return gax.OnCodes([]codes.Code{
codes.DeadlineExceeded,
codes.Unavailable,
}, gax.Backoff{
Initial: 1000 * time.Millisecond,
Max: 60000 * time.Millisecond,
Multiplier: 2.00,
})
}),
},
StartResumableWrite: []gax.CallOption{
gax.WithTimeout(60000 * time.Millisecond),
gax.WithRetry(func() gax.Retryer {
return gax.OnCodes([]codes.Code{
codes.DeadlineExceeded,
codes.Unavailable,
}, gax.Backoff{
Initial: 1000 * time.Millisecond,
Max: 60000 * time.Millisecond,
Multiplier: 2.00,
})
}),
},
QueryWriteStatus: []gax.CallOption{
gax.WithTimeout(60000 * time.Millisecond),
gax.WithRetry(func() gax.Retryer {
return gax.OnCodes([]codes.Code{
codes.DeadlineExceeded,
codes.Unavailable,
}, gax.Backoff{
Initial: 1000 * time.Millisecond,
Max: 60000 * time.Millisecond,
Multiplier: 2.00,
})
}),
},
GetServiceAccount: []gax.CallOption{
gax.WithTimeout(60000 * time.Millisecond),
gax.WithRetry(func() gax.Retryer {
return gax.OnCodes([]codes.Code{
codes.DeadlineExceeded,
codes.Unavailable,
}, gax.Backoff{
Initial: 1000 * time.Millisecond,
Max: 60000 * time.Millisecond,
Multiplier: 2.00,
})
}),
},
CreateHmacKey: []gax.CallOption{
gax.WithTimeout(60000 * time.Millisecond),
gax.WithRetry(func() gax.Retryer {
return gax.OnCodes([]codes.Code{
codes.DeadlineExceeded,
codes.Unavailable,
}, gax.Backoff{
Initial: 1000 * time.Millisecond,
Max: 60000 * time.Millisecond,
Multiplier: 2.00,
})
}),
},
DeleteHmacKey: []gax.CallOption{
gax.WithTimeout(60000 * time.Millisecond),
gax.WithRetry(func() gax.Retryer {
return gax.OnCodes([]codes.Code{
codes.DeadlineExceeded,
codes.Unavailable,
}, gax.Backoff{
Initial: 1000 * time.Millisecond,
Max: 60000 * time.Millisecond,
Multiplier: 2.00,
})
}),
},
GetHmacKey: []gax.CallOption{
gax.WithTimeout(60000 * time.Millisecond),
gax.WithRetry(func() gax.Retryer {
return gax.OnCodes([]codes.Code{
codes.DeadlineExceeded,
codes.Unavailable,
}, gax.Backoff{
Initial: 1000 * time.Millisecond,
Max: 60000 * time.Millisecond,
Multiplier: 2.00,
})
}),
},
ListHmacKeys: []gax.CallOption{
gax.WithTimeout(60000 * time.Millisecond),
gax.WithRetry(func() gax.Retryer {
return gax.OnCodes([]codes.Code{
codes.DeadlineExceeded,
codes.Unavailable,
}, gax.Backoff{
Initial: 1000 * time.Millisecond,
Max: 60000 * time.Millisecond,
Multiplier: 2.00,
})
}),
},
UpdateHmacKey: []gax.CallOption{
gax.WithTimeout(60000 * time.Millisecond),
gax.WithRetry(func() gax.Retryer {
return gax.OnCodes([]codes.Code{
codes.DeadlineExceeded,
codes.Unavailable,
}, gax.Backoff{
Initial: 1000 * time.Millisecond,
Max: 60000 * time.Millisecond,
Multiplier: 2.00,
})
}),
},
}
}
@ -296,13 +656,24 @@ func (c *Client) ComposeObject(ctx context.Context, req *storagepb.ComposeObject
return c.internalClient.ComposeObject(ctx, req, opts...)
}
// DeleteObject deletes an object and its metadata. Deletions are permanent if versioning
// is not enabled for the bucket, or if the generation parameter is used.
// DeleteObject deletes an object and its metadata.
//
// Deletions are normally permanent when versioning is disabled or whenever
// the generation parameter is used. However, if soft delete is enabled for
// the bucket, deleted objects can be restored using RestoreObject until the
// soft delete retention period has passed.
func (c *Client) DeleteObject(ctx context.Context, req *storagepb.DeleteObjectRequest, opts ...gax.CallOption) error {
return c.internalClient.DeleteObject(ctx, req, opts...)
}
// CancelResumableWrite cancels an in-progress resumable upload.
//
// Any attempts to write to the resumable upload after cancelling the upload
// will fail.
//
// The behavior for currently in progress write operations is not guaranteed -
// they could either complete before the cancellation or fail if the
// cancellation completes first.
func (c *Client) CancelResumableWrite(ctx context.Context, req *storagepb.CancelResumableWriteRequest, opts ...gax.CallOption) (*storagepb.CancelResumableWriteResponse, error) {
return c.internalClient.CancelResumableWrite(ctx, req, opts...)
}
@ -369,6 +740,9 @@ func (c *Client) UpdateObject(ctx context.Context, req *storagepb.UpdateObjectRe
// incur a performance cost over resuming at the correct write offset.
// This behavior can make client-side handling simpler in some cases.
//
// Clients must only send data that is a multiple of 256 KiB per message,
// unless the object is being finished with finish_write set to true.
//
// The service will not view the object as complete until the client has
// sent a WriteObjectRequest with finish_write set to true. Sending any
// requests on a stream after sending a request with finish_write set to
@ -455,9 +829,6 @@ type gRPCClient struct {
// Connection pool of gRPC connections to the service.
connPool gtransport.ConnPool
// flag to opt out of default deadlines via GOOGLE_API_GO_EXPERIMENTAL_DISABLE_DEFAULT_DEADLINE
disableDeadlines bool
// Points back to the CallOptions field of the containing Client
CallOptions **CallOptions
@ -503,11 +874,6 @@ func NewClient(ctx context.Context, opts ...option.ClientOption) (*Client, error
clientOpts = append(clientOpts, hookOpts...)
}
disableDeadlines, err := checkDisableDeadlines()
if err != nil {
return nil, err
}
connPool, err := gtransport.DialPool(ctx, append(clientOpts, opts...)...)
if err != nil {
return nil, err
@ -515,10 +881,9 @@ func NewClient(ctx context.Context, opts ...option.ClientOption) (*Client, error
client := Client{CallOptions: defaultCallOptions()}
c := &gRPCClient{
connPool: connPool,
disableDeadlines: disableDeadlines,
client: storagepb.NewStorageClient(connPool),
CallOptions: &client.CallOptions,
connPool: connPool,
client: storagepb.NewStorageClient(connPool),
CallOptions: &client.CallOptions,
}
c.setGoogleClientInfo()
@ -539,7 +904,7 @@ func (c *gRPCClient) Connection() *grpc.ClientConn {
// the `x-goog-api-client` header passed on each request. Intended for
// use by Google-written clients.
func (c *gRPCClient) setGoogleClientInfo(keyval ...string) {
kv := append([]string{"gl-go", versionGo()}, keyval...)
kv := append([]string{"gl-go", gax.GoVersion}, keyval...)
kv = append(kv, "gapic", getVersionClient(), "gax", gax.Version, "grpc", grpc.Version)
c.xGoogMetadata = metadata.Pairs("x-goog-api-client", gax.XGoogHeader(kv...))
}

View file

@ -15,4 +15,4 @@
package internal
// Version is the current tagged release of the library.
const Version = "1.30.1"
const Version = "1.31.0"

View file

@ -76,9 +76,14 @@ func setRetryHeaderHTTP(req interface{ Header() http.Header }) func(string, int)
return
}
header := req.Header()
// TODO(b/274504690): Consider dropping gccl-invocation-id key since it
// duplicates the X-Goog-Gcs-Idempotency-Token header (added in v1.31.0).
invocationHeader := fmt.Sprintf("gccl-invocation-id/%v gccl-attempt-count/%v", invocationID, attempts)
xGoogHeader := strings.Join([]string{invocationHeader, xGoogDefaultHeader}, " ")
header.Set("x-goog-api-client", xGoogHeader)
// Also use the invocationID for the idempotency token header, which will
// enable idempotent retries for more operations.
header.Set("x-goog-gcs-idempotency-token", invocationID)
}
}

View file

@ -21,7 +21,7 @@ import (
"regexp"
"cloud.google.com/go/internal/trace"
storagepb "cloud.google.com/go/storage/internal/apiv2/stubs"
"cloud.google.com/go/storage/internal/apiv2/storagepb"
raw "google.golang.org/api/storage/v1"
)

View file

@ -57,7 +57,7 @@ func WithJSONReads() option.ClientOption {
}
// WithXMLReads is an option that may be passed to a Storage Client on creation.
// It sets the client to use the JSON API for object reads.
// It sets the client to use the XML API for object reads.
//
// This is the current default.
func WithXMLReads() option.ClientOption {

View file

@ -113,6 +113,12 @@ type PostPolicyV4Options struct {
// Optional.
Conditions []PostPolicyV4Condition
// Hostname sets the host of the signed post policy. This field overrides
// any endpoint set on a storage Client or through STORAGE_EMULATOR_HOST.
// Only compatible with PathStyle URLStyle.
// Optional.
Hostname string
shouldHashSignBytes bool
}
@ -128,6 +134,7 @@ func (opts *PostPolicyV4Options) clone() *PostPolicyV4Options {
Fields: opts.Fields,
Conditions: opts.Conditions,
shouldHashSignBytes: opts.shouldHashSignBytes,
Hostname: opts.Hostname,
}
}
@ -370,7 +377,7 @@ func GenerateSignedPostPolicyV4(bucket, object string, opts *PostPolicyV4Options
u := &url.URL{
Path: path,
RawPath: pathEncodeV4(path),
Host: opts.Style.host(bucket),
Host: opts.Style.host(opts.Hostname, bucket),
Scheme: scheme,
}

View file

@ -41,7 +41,7 @@ import (
"cloud.google.com/go/internal/optional"
"cloud.google.com/go/internal/trace"
"cloud.google.com/go/storage/internal"
storagepb "cloud.google.com/go/storage/internal/apiv2/stubs"
"cloud.google.com/go/storage/internal/apiv2/storagepb"
"github.com/googleapis/gax-go/v2"
"golang.org/x/oauth2/google"
"google.golang.org/api/googleapi"
@ -109,8 +109,8 @@ type Client struct {
raw *raw.Service
// Scheme describes the scheme under the current host.
scheme string
// ReadHost is the default host used on the reader.
readHost string
// xmlHost is the default host used for XML requests.
xmlHost string
// May be nil.
creds *google.Credentials
retry *retryConfig
@ -199,7 +199,7 @@ func NewClient(ctx context.Context, opts ...option.ClientOption) (*Client, error
if err != nil {
return nil, fmt.Errorf("storage client: %w", err)
}
// Update readHost and scheme with the chosen endpoint.
// Update xmlHost and scheme with the chosen endpoint.
u, err := url.Parse(ep)
if err != nil {
return nil, fmt.Errorf("supplied endpoint %q is not valid: %w", ep, err)
@ -211,12 +211,12 @@ func NewClient(ctx context.Context, opts ...option.ClientOption) (*Client, error
}
return &Client{
hc: hc,
raw: rawService,
scheme: u.Scheme,
readHost: u.Host,
creds: creds,
tc: tc,
hc: hc,
raw: rawService,
scheme: u.Scheme,
xmlHost: u.Host,
creds: creds,
tc: tc,
}, nil
}
@ -262,13 +262,13 @@ const (
SigningSchemeV4
)
// URLStyle determines the style to use for the signed URL. pathStyle is the
// URLStyle determines the style to use for the signed URL. PathStyle is the
// default. All non-default options work with V4 scheme only. See
// https://cloud.google.com/storage/docs/request-endpoints for details.
type URLStyle interface {
// host should return the host portion of the signed URL, not including
// the scheme (e.g. storage.googleapis.com).
host(bucket string) string
host(hostname, bucket string) string
// path should return the path portion of the signed URL, which may include
// both the bucket and object name or only the object name depending on the
@ -284,7 +284,11 @@ type bucketBoundHostname struct {
hostname string
}
func (s pathStyle) host(bucket string) string {
func (s pathStyle) host(hostname, bucket string) string {
if hostname != "" {
return stripScheme(hostname)
}
if host := os.Getenv("STORAGE_EMULATOR_HOST"); host != "" {
return stripScheme(host)
}
@ -292,7 +296,7 @@ func (s pathStyle) host(bucket string) string {
return "storage.googleapis.com"
}
func (s virtualHostedStyle) host(bucket string) string {
func (s virtualHostedStyle) host(_, bucket string) string {
if host := os.Getenv("STORAGE_EMULATOR_HOST"); host != "" {
return bucket + "." + stripScheme(host)
}
@ -300,7 +304,7 @@ func (s virtualHostedStyle) host(bucket string) string {
return bucket + ".storage.googleapis.com"
}
func (s bucketBoundHostname) host(bucket string) string {
func (s bucketBoundHostname) host(_, bucket string) string {
return s.hostname
}
@ -321,7 +325,10 @@ func (s bucketBoundHostname) path(bucket, object string) string {
}
// PathStyle is the default style, and will generate a URL of the form
// "storage.googleapis.com/<bucket-name>/<object-name>".
// "<host-name>/<bucket-name>/<object-name>". By default, <host-name> is
// storage.googleapis.com, but setting an endpoint on the storage Client or
// through STORAGE_EMULATOR_HOST overrides this. Setting Hostname on
// SignedURLOptions or PostPolicyV4Options overrides everything else.
func PathStyle() URLStyle {
return pathStyle{}
}
@ -442,6 +449,12 @@ type SignedURLOptions struct {
// Scheme determines the version of URL signing to use. Default is
// SigningSchemeV2.
Scheme SigningScheme
// Hostname sets the host of the signed URL. This field overrides any
// endpoint set on a storage Client or through STORAGE_EMULATOR_HOST.
// Only compatible with PathStyle URLStyle.
// Optional.
Hostname string
}
func (opts *SignedURLOptions) clone() *SignedURLOptions {
@ -458,6 +471,7 @@ func (opts *SignedURLOptions) clone() *SignedURLOptions {
Style: opts.Style,
Insecure: opts.Insecure,
Scheme: opts.Scheme,
Hostname: opts.Hostname,
}
}
@ -716,7 +730,7 @@ func signedURLV4(bucket, name string, opts *SignedURLOptions, now time.Time) (st
fmt.Fprintf(buf, "%s\n", escapedQuery)
// Fill in the hostname based on the desired URL style.
u.Host = opts.Style.host(bucket)
u.Host = opts.Style.host(opts.Hostname, bucket)
// Fill in the URL scheme.
if opts.Insecure {
@ -850,7 +864,7 @@ func signedURLV2(bucket, name string, opts *SignedURLOptions) (string, error) {
}
encoded := base64.StdEncoding.EncodeToString(b)
u.Scheme = "https"
u.Host = PathStyle().host(bucket)
u.Host = PathStyle().host(opts.Hostname, bucket)
q := u.Query()
q.Set("GoogleAccessId", opts.GoogleAccessID)
q.Set("Expires", fmt.Sprintf("%d", opts.Expires.Unix()))
@ -893,7 +907,9 @@ func (o *ObjectHandle) Generation(gen int64) *ObjectHandle {
}
// If returns a new ObjectHandle that applies a set of preconditions.
// Preconditions already set on the ObjectHandle are ignored.
// Preconditions already set on the ObjectHandle are ignored. The supplied
// Conditions must have at least one field set to a non-default value;
// otherwise an error will be returned from any operation on the ObjectHandle.
// Operations on the new handle will return an error if the preconditions are not
// satisfied. See https://cloud.google.com/storage/docs/generations-preconditions
// for more details.
@ -1163,7 +1179,7 @@ func (uattrs *ObjectAttrsToUpdate) toProtoObject(bucket, object string) *storage
o.Acl = toProtoObjectACL(uattrs.ACL)
}
// TODO(cathyo): Handle metadata. Pending b/230510191.
o.Metadata = uattrs.Metadata
return o
}
@ -1484,6 +1500,8 @@ type Query struct {
// aside from the prefix, contain delimiter will have their name,
// truncated after the delimiter, returned in prefixes.
// Duplicate prefixes are omitted.
// Must be set to / when used with the MatchGlob parameter to filter results
// in a directory-like mode.
// Optional.
Delimiter string
@ -1497,9 +1515,9 @@ type Query struct {
Versions bool
// attrSelection is used to select only specific fields to be returned by
// the query. It is set by the user calling calling SetAttrSelection. These
// the query. It is set by the user calling SetAttrSelection. These
// are used by toFieldMask and toFieldSelection for gRPC and HTTP/JSON
// clients repsectively.
// clients respectively.
attrSelection []string
// StartOffset is used to filter results to objects whose names are
@ -1525,6 +1543,12 @@ type Query struct {
// true, they will also be included as objects and their metadata will be
// populated in the returned ObjectAttrs.
IncludeTrailingDelimiter bool
// MatchGlob is a glob pattern used to filter results (for example, foo*bar). See
// https://cloud.google.com/storage/docs/json_api/v1/objects/list#list-object-glob
// for syntax details. When Delimiter is set in conjunction with MatchGlob,
// it must be set to /.
MatchGlob string
}
// attrToFieldMap maps the field names of ObjectAttrs to the underlying field

View file

@ -86,7 +86,7 @@ type Writer struct {
// cancellation.
ChunkRetryDeadline time.Duration
// ProgressFunc can be used to monitor the progress of a large write.
// ProgressFunc can be used to monitor the progress of a large write
// operation. If ProgressFunc is not nil and writing requires multiple
// calls to the underlying service (see
// https://cloud.google.com/storage/docs/json_api/v1/how-tos/resumable-upload),

View file

@ -1,5 +1,12 @@
# Release History
## 1.6.1 (2023-06-06)
### Bugs Fixed
* Retry policy always clones the underlying `*http.Request` before invoking the next policy.
* Added some non-standard error codes to the list of error codes for unregistered resource providers.
* Fixed an issue in `azcore.NewClient()` and `arm.NewClient()` that could cause an incorrect module name to be used in telemetry.
## 1.6.0 (2023-05-04)
### Features Added

View file

@ -76,12 +76,13 @@ type Client struct {
}
// NewClient creates a new Client instance with the provided values.
// - clientName - the fully qualified name of the client ("package.Client"); this is used by the tracing provider when creating spans
// - clientName - the fully qualified name of the client ("module/package.Client"); this is used by the telemetry policy and tracing provider.
// if module and package are the same value, the "module/" prefix can be omitted.
// - moduleVersion - the semantic version of the containing module; used by the telemetry policy
// - plOpts - pipeline configuration options; can be the zero-value
// - options - optional client configurations; pass nil to accept the default values
func NewClient(clientName, moduleVersion string, plOpts runtime.PipelineOptions, options *ClientOptions) (*Client, error) {
pkg, err := shared.ExtractPackageName(clientName)
mod, client, err := shared.ExtractModuleName(clientName)
if err != nil {
return nil, err
}
@ -96,9 +97,9 @@ func NewClient(clientName, moduleVersion string, plOpts runtime.PipelineOptions,
}
}
pl := runtime.NewPipeline(pkg, moduleVersion, plOpts, options)
pl := runtime.NewPipeline(mod, moduleVersion, plOpts, options)
tr := options.TracingProvider.NewTracer(clientName, moduleVersion)
tr := options.TracingProvider.NewTracer(client, moduleVersion)
return &Client{pl: pl, tr: tr}, nil
}

View file

@ -32,5 +32,5 @@ const (
Module = "azcore"
// Version is the semantic version (see http://semver.org) of this module.
Version = "v1.6.0"
Version = "v1.6.1"
)

View file

@ -13,7 +13,6 @@ import (
"reflect"
"regexp"
"strconv"
"strings"
"time"
)
@ -79,14 +78,26 @@ func ValidateModVer(moduleVersion string) error {
return nil
}
// ExtractPackageName returns "package" from "package.Client".
// ExtractModuleName returns "module", "package.Client" from "module/package.Client" or
// "package", "package.Client" from "package.Client" when there's no "module/" prefix.
// If clientName is malformed, an error is returned.
func ExtractPackageName(clientName string) (string, error) {
pkg, client, ok := strings.Cut(clientName, ".")
if !ok {
return "", fmt.Errorf("missing . in clientName %s", clientName)
} else if pkg == "" || client == "" {
return "", fmt.Errorf("malformed clientName %s", clientName)
func ExtractModuleName(clientName string) (string, string, error) {
// uses unnamed capturing for "module", "package.Client", and "package"
regex, err := regexp.Compile(`^(?:([a-z0-9]+)/)?(([a-z0-9]+)\.(?:[A-Za-z0-9]+))$`)
if err != nil {
return "", "", err
}
return pkg, nil
matches := regex.FindStringSubmatch(clientName)
if len(matches) < 4 {
return "", "", fmt.Errorf("malformed clientName %s", clientName)
}
// the first match is the entire string, the second is "module", the third is
// "package.Client" and the fourth is "package".
// if there was no "module/" prefix, the second match will be the empty string
if matches[1] != "" {
return matches[1], matches[2], nil
}
return matches[3], matches[2], nil
}

View file

@ -125,7 +125,8 @@ func (p *retryPolicy) Do(req *policy.Request) (resp *http.Response, err error) {
}
if options.TryTimeout == 0 {
resp, err = req.Next()
clone := req.Clone(req.Raw().Context())
resp, err = clone.Next()
} else {
// Set the per-try time for this particular retry operation and then Do the operation.
tryCtx, tryCancel := context.WithTimeout(req.Raw().Context(), options.TryTimeout)

View file

@ -0,0 +1,409 @@
# Release History
## 1.3.0 (2023-05-09)
### Breaking Changes
> These changes affect only code written against a beta version such as v1.3.0-beta.5
* Renamed `NewOnBehalfOfCredentialFromCertificate` to `NewOnBehalfOfCredentialWithCertificate`
* Renamed `NewOnBehalfOfCredentialFromSecret` to `NewOnBehalfOfCredentialWithSecret`
### Other Changes
* Upgraded to MSAL v1.0.0
## 1.3.0-beta.5 (2023-04-11)
### Breaking Changes
> These changes affect only code written against a beta version such as v1.3.0-beta.4
* Moved `NewWorkloadIdentityCredential()` parameters into `WorkloadIdentityCredentialOptions`.
The constructor now reads default configuration from environment variables set by the Azure
workload identity webhook by default.
([#20478](https://github.com/Azure/azure-sdk-for-go/pull/20478))
* Removed CAE support. It will return in v1.4.0-beta.1
([#20479](https://github.com/Azure/azure-sdk-for-go/pull/20479))
### Bugs Fixed
* Fixed an issue in `DefaultAzureCredential` that could cause the managed identity endpoint check to fail in rare circumstances.
## 1.3.0-beta.4 (2023-03-08)
### Features Added
* Added `WorkloadIdentityCredentialOptions.AdditionallyAllowedTenants` and `.DisableInstanceDiscovery`
### Bugs Fixed
* Credentials now synchronize within `GetToken()` so a single instance can be shared among goroutines
([#20044](https://github.com/Azure/azure-sdk-for-go/issues/20044))
### Other Changes
* Upgraded dependencies
## 1.2.2 (2023-03-07)
### Other Changes
* Upgraded dependencies
## 1.3.0-beta.3 (2023-02-07)
### Features Added
* By default, credentials set client capability "CP1" to enable support for
[Continuous Access Evaluation (CAE)](https://docs.microsoft.com/azure/active-directory/develop/app-resilience-continuous-access-evaluation).
This indicates to Azure Active Directory that your application can handle CAE claims challenges.
You can disable this behavior by setting the environment variable "AZURE_IDENTITY_DISABLE_CP1" to "true".
* `InteractiveBrowserCredentialOptions.LoginHint` enables pre-populating the login
prompt with a username ([#15599](https://github.com/Azure/azure-sdk-for-go/pull/15599))
* Service principal and user credentials support ADFS authentication on Azure Stack.
Specify "adfs" as the credential's tenant.
* Applications running in private or disconnected clouds can prevent credentials from
requesting Azure AD instance metadata by setting the `DisableInstanceDiscovery`
field on credential options.
* Many credentials can now be configured to authenticate in multiple tenants. The
options types for these credentials have an `AdditionallyAllowedTenants` field
that specifies additional tenants in which the credential may authenticate.
## 1.3.0-beta.2 (2023-01-10)
### Features Added
* Added `OnBehalfOfCredential` to support the on-behalf-of flow
([#16642](https://github.com/Azure/azure-sdk-for-go/issues/16642))
### Bugs Fixed
* `AzureCLICredential` reports token expiration in local time (should be UTC)
### Other Changes
* `AzureCLICredential` imposes its default timeout only when the `Context`
passed to `GetToken()` has no deadline
* Added `NewCredentialUnavailableError()`. This function constructs an error indicating
a credential can't authenticate and an encompassing `ChainedTokenCredential` should
try its next credential, if any.
## 1.3.0-beta.1 (2022-12-13)
### Features Added
* `WorkloadIdentityCredential` and `DefaultAzureCredential` support
Workload Identity Federation on Kubernetes. `DefaultAzureCredential`
support requires environment variable configuration as set by the
Workload Identity webhook.
([#15615](https://github.com/Azure/azure-sdk-for-go/issues/15615))
## 1.2.0 (2022-11-08)
### Other Changes
* This version includes all fixes and features from 1.2.0-beta.*
## 1.2.0-beta.3 (2022-10-11)
### Features Added
* `ManagedIdentityCredential` caches tokens in memory
### Bugs Fixed
* `ClientCertificateCredential` sends only the leaf cert for SNI authentication
## 1.2.0-beta.2 (2022-08-10)
### Features Added
* Added `ClientAssertionCredential` to enable applications to authenticate
with custom client assertions
### Other Changes
* Updated AuthenticationFailedError with links to TROUBLESHOOTING.md for relevant errors
* Upgraded `microsoft-authentication-library-for-go` requirement to v0.6.0
## 1.2.0-beta.1 (2022-06-07)
### Features Added
* `EnvironmentCredential` reads certificate passwords from `AZURE_CLIENT_CERTIFICATE_PASSWORD`
([#17099](https://github.com/Azure/azure-sdk-for-go/pull/17099))
## 1.1.0 (2022-06-07)
### Features Added
* `ClientCertificateCredential` and `ClientSecretCredential` support ESTS-R. First-party
applications can set environment variable `AZURE_REGIONAL_AUTHORITY_NAME` with a
region name.
([#15605](https://github.com/Azure/azure-sdk-for-go/issues/15605))
## 1.0.1 (2022-06-07)
### Other Changes
* Upgrade `microsoft-authentication-library-for-go` requirement to v0.5.1
([#18176](https://github.com/Azure/azure-sdk-for-go/issues/18176))
## 1.0.0 (2022-05-12)
### Features Added
* `DefaultAzureCredential` reads environment variable `AZURE_CLIENT_ID` for the
client ID of a user-assigned managed identity
([#17293](https://github.com/Azure/azure-sdk-for-go/pull/17293))
### Breaking Changes
* Removed `AuthorizationCodeCredential`. Use `InteractiveBrowserCredential` instead
to authenticate a user with the authorization code flow.
* Instances of `AuthenticationFailedError` are now returned by pointer.
* `GetToken()` returns `azcore.AccessToken` by value
### Bugs Fixed
* `AzureCLICredential` panics after receiving an unexpected error type
([#17490](https://github.com/Azure/azure-sdk-for-go/issues/17490))
### Other Changes
* `GetToken()` returns an error when the caller specifies no scope
* Updated to the latest versions of `golang.org/x/crypto`, `azcore` and `internal`
## 0.14.0 (2022-04-05)
### Breaking Changes
* This module now requires Go 1.18
* Removed `AuthorityHost`. Credentials are now configured for sovereign or private
clouds with the API in `azcore/cloud`, for example:
```go
// before
opts := azidentity.ClientSecretCredentialOptions{AuthorityHost: azidentity.AzureGovernment}
cred, err := azidentity.NewClientSecretCredential(tenantID, clientID, secret, &opts)
// after
import "github.com/Azure/azure-sdk-for-go/sdk/azcore/cloud"
opts := azidentity.ClientSecretCredentialOptions{}
opts.Cloud = cloud.AzureGovernment
cred, err := azidentity.NewClientSecretCredential(tenantID, clientID, secret, &opts)
```
## 0.13.2 (2022-03-08)
### Bugs Fixed
* Prevented a data race in `DefaultAzureCredential` and `ChainedTokenCredential`
([#17144](https://github.com/Azure/azure-sdk-for-go/issues/17144))
### Other Changes
* Upgraded App Service managed identity version from 2017-09-01 to 2019-08-01
([#17086](https://github.com/Azure/azure-sdk-for-go/pull/17086))
## 0.13.1 (2022-02-08)
### Features Added
* `EnvironmentCredential` supports certificate SNI authentication when
`AZURE_CLIENT_SEND_CERTIFICATE_CHAIN` is "true".
([#16851](https://github.com/Azure/azure-sdk-for-go/pull/16851))
### Bugs Fixed
* `ManagedIdentityCredential.GetToken()` now returns an error when configured for
a user assigned identity in Azure Cloud Shell (which doesn't support such identities)
([#16946](https://github.com/Azure/azure-sdk-for-go/pull/16946))
### Other Changes
* `NewDefaultAzureCredential()` logs non-fatal errors. These errors are also included in the
error returned by `DefaultAzureCredential.GetToken()` when it's unable to acquire a token
from any source. ([#15923](https://github.com/Azure/azure-sdk-for-go/issues/15923))
## 0.13.0 (2022-01-11)
### Breaking Changes
* Replaced `AuthenticationFailedError.RawResponse()` with a field having the same name
* Unexported `CredentialUnavailableError`
* Instances of `ChainedTokenCredential` will now skip looping through the list of source credentials and re-use the first successful credential on subsequent calls to `GetToken`.
* If `ChainedTokenCredentialOptions.RetrySources` is true, `ChainedTokenCredential` will continue to try all of the originally provided credentials each time the `GetToken` method is called.
* `ChainedTokenCredential.successfulCredential` will contain a reference to the last successful credential.
* `DefaultAzureCredenial` will also re-use the first successful credential on subsequent calls to `GetToken`.
* `DefaultAzureCredential.chain.successfulCredential` will also contain a reference to the last successful credential.
### Other Changes
* `ManagedIdentityCredential` no longer probes IMDS before requesting a token
from it. Also, an error response from IMDS no longer disables a credential
instance. Following an error, a credential instance will continue to send
requests to IMDS as necessary.
* Adopted MSAL for user and service principal authentication
* Updated `azcore` requirement to 0.21.0
## 0.12.0 (2021-11-02)
### Breaking Changes
* Raised minimum go version to 1.16
* Removed `NewAuthenticationPolicy()` from credentials. Clients should instead use azcore's
`runtime.NewBearerTokenPolicy()` to construct a bearer token authorization policy.
* The `AuthorityHost` field in credential options structs is now a custom type,
`AuthorityHost`, with underlying type `string`
* `NewChainedTokenCredential` has a new signature to accommodate a placeholder
options struct:
```go
// before
cred, err := NewChainedTokenCredential(credA, credB)
// after
cred, err := NewChainedTokenCredential([]azcore.TokenCredential{credA, credB}, nil)
```
* Removed `ExcludeAzureCLICredential`, `ExcludeEnvironmentCredential`, and `ExcludeMSICredential`
from `DefaultAzureCredentialOptions`
* `NewClientCertificateCredential` requires a `[]*x509.Certificate` and `crypto.PrivateKey` instead of
a path to a certificate file. Added `ParseCertificates` to simplify getting these in common cases:
```go
// before
cred, err := NewClientCertificateCredential("tenant", "client-id", "/cert.pem", nil)
// after
certData, err := os.ReadFile("/cert.pem")
certs, key, err := ParseCertificates(certData, password)
cred, err := NewClientCertificateCredential(tenantID, clientID, certs, key, nil)
```
* Removed `InteractiveBrowserCredentialOptions.ClientSecret` and `.Port`
* Removed `AADAuthenticationFailedError`
* Removed `id` parameter of `NewManagedIdentityCredential()`. User assigned identities are now
specified by `ManagedIdentityCredentialOptions.ID`:
```go
// before
cred, err := NewManagedIdentityCredential("client-id", nil)
// or, for a resource ID
opts := &ManagedIdentityCredentialOptions{ID: ResourceID}
cred, err := NewManagedIdentityCredential("/subscriptions/...", opts)
// after
clientID := ClientID("7cf7db0d-...")
opts := &ManagedIdentityCredentialOptions{ID: clientID}
// or, for a resource ID
resID: ResourceID("/subscriptions/...")
opts := &ManagedIdentityCredentialOptions{ID: resID}
cred, err := NewManagedIdentityCredential(opts)
```
* `DeviceCodeCredentialOptions.UserPrompt` has a new type: `func(context.Context, DeviceCodeMessage) error`
* Credential options structs now embed `azcore.ClientOptions`. In addition to changing literal initialization
syntax, this change renames `HTTPClient` fields to `Transport`.
* Renamed `LogCredential` to `EventCredential`
* `AzureCLICredential` no longer reads the environment variable `AZURE_CLI_PATH`
* `NewManagedIdentityCredential` no longer reads environment variables `AZURE_CLIENT_ID` and
`AZURE_RESOURCE_ID`. Use `ManagedIdentityCredentialOptions.ID` instead.
* Unexported `AuthenticationFailedError` and `CredentialUnavailableError` structs. In their place are two
interfaces having the same names.
### Bugs Fixed
* `AzureCLICredential.GetToken` no longer mutates its `opts.Scopes`
### Features Added
* Added connection configuration options to `DefaultAzureCredentialOptions`
* `AuthenticationFailedError.RawResponse()` returns the HTTP response motivating the error,
if available
### Other Changes
* `NewDefaultAzureCredential()` returns `*DefaultAzureCredential` instead of `*ChainedTokenCredential`
* Added `TenantID` field to `DefaultAzureCredentialOptions` and `AzureCLICredentialOptions`
## 0.11.0 (2021-09-08)
### Breaking Changes
* Unexported `AzureCLICredentialOptions.TokenProvider` and its type,
`AzureCLITokenProvider`
### Bug Fixes
* `ManagedIdentityCredential.GetToken` returns `CredentialUnavailableError`
when IMDS has no assigned identity, signaling `DefaultAzureCredential` to
try other credentials
## 0.10.0 (2021-08-30)
### Breaking Changes
* Update based on `azcore` refactor [#15383](https://github.com/Azure/azure-sdk-for-go/pull/15383)
## 0.9.3 (2021-08-20)
### Bugs Fixed
* `ManagedIdentityCredential.GetToken` no longer mutates its `opts.Scopes`
### Other Changes
* Bumps version of `azcore` to `v0.18.1`
## 0.9.2 (2021-07-23)
### Features Added
* Adding support for Service Fabric environment in `ManagedIdentityCredential`
* Adding an option for using a resource ID instead of client ID in `ManagedIdentityCredential`
## 0.9.1 (2021-05-24)
### Features Added
* Add LICENSE.txt and bump version information
## 0.9.0 (2021-05-21)
### Features Added
* Add support for authenticating in Azure Stack environments
* Enable user assigned identities for the IMDS scenario in `ManagedIdentityCredential`
* Add scope to resource conversion in `GetToken()` on `ManagedIdentityCredential`
## 0.8.0 (2021-01-20)
### Features Added
* Updating documentation
## 0.7.1 (2021-01-04)
### Features Added
* Adding port option to `InteractiveBrowserCredential`
## 0.7.0 (2020-12-11)
### Features Added
* Add `redirectURI` parameter back to authentication code flow
## 0.6.1 (2020-12-09)
### Features Added
* Updating query parameter in `ManagedIdentityCredential` and updating datetime string for parsing managed identity access tokens.
## 0.6.0 (2020-11-16)
### Features Added
* Remove `RedirectURL` parameter from auth code flow to align with the MSAL implementation which relies on the native client redirect URL.
## 0.5.0 (2020-10-30)
### Features Added
* Flattening credential options
## 0.4.3 (2020-10-21)
### Features Added
* Adding Azure Arc support in `ManagedIdentityCredential`
## 0.4.2 (2020-10-16)
### Features Added
* Typo fixes
## 0.4.1 (2020-10-16)
### Features Added
* Ensure authority hosts are only HTTPs
## 0.4.0 (2020-10-16)
### Features Added
* Adding options structs for credentials
## 0.3.0 (2020-10-09)
### Features Added
* Update `DeviceCodeCredential` callback
## 0.2.2 (2020-10-09)
### Features Added
* Add `AuthorizationCodeCredential`
## 0.2.1 (2020-10-06)
### Features Added
* Add `InteractiveBrowserCredential`
## 0.2.0 (2020-09-11)
### Features Added
* Refactor `azidentity` on top of `azcore` refactor
* Updated policies to conform to `policy.Policy` interface changes.
* Updated non-retriable errors to conform to `azcore.NonRetriableError`.
* Fixed calls to `Request.SetBody()` to include content type.
* Switched endpoints to string types and removed extra parsing code.
## 0.1.1 (2020-09-02)
### Features Added
* Add `AzureCLICredential` to `DefaultAzureCredential` chain
## 0.1.0 (2020-07-23)
### Features Added
* Initial Release. Azure Identity library that provides Azure Active Directory token authentication support for the SDK.

View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) Microsoft Corporation.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE

View file

@ -0,0 +1,307 @@
# Migrating from autorest/adal to azidentity
`azidentity` provides Azure Active Directory (Azure AD) authentication for the newest Azure SDK modules (`github.com/azure-sdk-for-go/sdk/...`). Older Azure SDK packages (`github.com/azure-sdk-for-go/services/...`) use types from `github.com/go-autorest/autorest/adal` instead.
This guide shows common authentication code using `autorest/adal` and its equivalent using `azidentity`.
## Table of contents
- [Acquire a token](#acquire-a-token)
- [Client certificate authentication](#client-certificate-authentication)
- [Client secret authentication](#client-secret-authentication)
- [Configuration](#configuration)
- [Device code authentication](#device-code-authentication)
- [Managed identity](#managed-identity)
- [Use azidentity credentials with older packages](#use-azidentity-credentials-with-older-packages)
## Configuration
### `autorest/adal`
Token providers require a token audience (resource identifier) and an instance of `adal.OAuthConfig`, which requires an Azure AD endpoint and tenant:
```go
import "github.com/Azure/go-autorest/autorest/adal"
oauthCfg, err := adal.NewOAuthConfig("https://login.chinacloudapi.cn", tenantID)
handle(err)
spt, err := adal.NewServicePrincipalTokenWithSecret(
*oauthCfg, clientID, "https://management.chinacloudapi.cn/", &adal.ServicePrincipalTokenSecret{ClientSecret: secret},
)
```
### `azidentity`
A credential instance can acquire tokens for any audience. The audience for each token is determined by the client requesting it. Credentials require endpoint configuration only for sovereign or private clouds. The `azcore/cloud` package has predefined configuration for sovereign clouds such as Azure China:
```go
import (
"github.com/Azure/azure-sdk-for-go/sdk/azcore/cloud"
"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
)
clientOpts := azcore.ClientOptions{Cloud: cloud.AzureChina}
cred, err := azidentity.NewClientSecretCredential(
tenantID, clientID, secret, &azidentity.ClientSecretCredentialOptions{ClientOptions: clientOpts},
)
handle(err)
```
## Client secret authentication
### `autorest/adal`
```go
import (
"github.com/Azure/azure-sdk-for-go/services/resources/mgmt/2018-06-01/subscriptions"
"github.com/Azure/go-autorest/autorest"
"github.com/Azure/go-autorest/autorest/adal"
)
oauthCfg, err := adal.NewOAuthConfig("https://login.microsoftonline.com", tenantID)
handle(err)
spt, err := adal.NewServicePrincipalTokenWithSecret(
*oauthCfg, clientID, "https://management.azure.com/", &adal.ServicePrincipalTokenSecret{ClientSecret: secret},
)
handle(err)
client := subscriptions.NewClient()
client.Authorizer = autorest.NewBearerAuthorizer(spt)
```
### `azidentity`
```go
import (
"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armsubscriptions"
)
cred, err := azidentity.NewClientSecretCredential(tenantID, clientID, secret, nil)
handle(err)
client, err := armsubscriptions.NewClient(cred, nil)
handle(err)
```
## Client certificate authentication
### `autorest/adal`
```go
import (
"os"
"github.com/Azure/azure-sdk-for-go/services/resources/mgmt/2018-06-01/subscriptions"
"github.com/Azure/go-autorest/autorest"
"github.com/Azure/go-autorest/autorest/adal"
)
certData, err := os.ReadFile("./example.pfx")
handle(err)
certificate, rsaPrivateKey, err := decodePkcs12(certData, "")
handle(err)
oauthCfg, err := adal.NewOAuthConfig("https://login.microsoftonline.com", tenantID)
handle(err)
spt, err := adal.NewServicePrincipalTokenFromCertificate(
*oauthConfig, clientID, certificate, rsaPrivateKey, "https://management.azure.com/",
)
client := subscriptions.NewClient()
client.Authorizer = autorest.NewBearerAuthorizer(spt)
```
### `azidentity`
```go
import (
"os"
"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armsubscriptions"
)
certData, err := os.ReadFile("./example.pfx")
handle(err)
certs, key, err := azidentity.ParseCertificates(certData, nil)
handle(err)
cred, err = azidentity.NewClientCertificateCredential(tenantID, clientID, certs, key, nil)
handle(err)
client, err := armsubscriptions.NewClient(cred, nil)
handle(err)
```
## Managed identity
### `autorest/adal`
```go
import (
"github.com/Azure/azure-sdk-for-go/services/resources/mgmt/2018-06-01/subscriptions"
"github.com/Azure/go-autorest/autorest"
"github.com/Azure/go-autorest/autorest/adal"
)
spt, err := adal.NewServicePrincipalTokenFromManagedIdentity("https://management.azure.com/", nil)
handle(err)
client := subscriptions.NewClient()
client.Authorizer = autorest.NewBearerAuthorizer(spt)
```
### `azidentity`
```go
import (
"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armsubscriptions"
)
cred, err := azidentity.NewManagedIdentityCredential(nil)
handle(err)
client, err := armsubscriptions.NewClient(cred, nil)
handle(err)
```
### User-assigned identities
`autorest/adal`:
```go
import "github.com/Azure/go-autorest/autorest/adal"
opts := &adal.ManagedIdentityOptions{ClientID: "..."}
spt, err := adal.NewServicePrincipalTokenFromManagedIdentity("https://management.azure.com/")
handle(err)
```
`azidentity`:
```go
import "github.com/Azure/azure-sdk-for-go/sdk/azidentity"
opts := azidentity.ManagedIdentityCredentialOptions{ID: azidentity.ClientID("...")}
cred, err := azidentity.NewManagedIdentityCredential(&opts)
handle(err)
```
## Device code authentication
### `autorest/adal`
```go
import (
"fmt"
"net/http"
"github.com/Azure/azure-sdk-for-go/services/resources/mgmt/2018-06-01/subscriptions"
"github.com/Azure/go-autorest/autorest"
"github.com/Azure/go-autorest/autorest/adal"
)
oauthClient := &http.Client{}
oauthCfg, err := adal.NewOAuthConfig("https://login.microsoftonline.com", tenantID)
handle(err)
resource := "https://management.azure.com/"
deviceCode, err := adal.InitiateDeviceAuth(oauthClient, *oauthCfg, clientID, resource)
handle(err)
// display instructions, wait for the user to authenticate
fmt.Println(*deviceCode.Message)
token, err := adal.WaitForUserCompletion(oauthClient, deviceCode)
handle(err)
spt, err := adal.NewServicePrincipalTokenFromManualToken(*oauthCfg, clientID, resource, *token)
handle(err)
client := subscriptions.NewClient()
client.Authorizer = autorest.NewBearerAuthorizer(spt)
```
### `azidentity`
```go
import (
"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armsubscriptions"
)
cred, err := azidentity.NewDeviceCodeCredential(nil)
handle(err)
client, err := armsubscriptions.NewSubscriptionsClient(cred, nil)
handle(err)
```
`azidentity.DeviceCodeCredential` will guide a user through authentication, printing instructions to the console by default. The user prompt is customizable. For more information, see the [package documentation](https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/azidentity#DeviceCodeCredential).
## Acquire a token
### `autorest/adal`
```go
import "github.com/Azure/go-autorest/autorest/adal"
oauthCfg, err := adal.NewOAuthConfig("https://login.microsoftonline.com", tenantID)
handle(err)
spt, err := adal.NewServicePrincipalTokenWithSecret(
*oauthCfg, clientID, "https://vault.azure.net", &adal.ServicePrincipalTokenSecret{ClientSecret: secret},
)
err = spt.Refresh()
if err == nil {
token := spt.Token
}
```
### `azidentity`
In ordinary usage, application code doesn't need to request tokens from credentials directly. Azure SDK clients handle token acquisition and refreshing internally. However, applications may call `GetToken()` to do so. All credential types have this method.
```go
import (
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
)
cred, err := azidentity.NewClientSecretCredential(tenantID, clientID, secret, nil)
handle(err)
tk, err := cred.GetToken(
context.TODO(), policy.TokenRequestOptions{Scopes: []string{"https://vault.azure.net/.default"}},
)
if err == nil {
token := tk.Token
}
```
Note that `azidentity` credentials use the Azure AD v2.0 endpoint, which requires OAuth 2 scopes instead of the resource identifiers `autorest/adal` expects. For more information, see [Azure AD documentation](https://docs.microsoft.com/azure/active-directory/develop/v2-permissions-and-consent).
## Use azidentity credentials with older packages
The [azidext module](https://pkg.go.dev/github.com/jongio/azidext/go/azidext) provides an adapter for `azidentity` credential types. The adapter enables using the credential types with older Azure SDK clients. For example:
```go
import (
"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
"github.com/Azure/azure-sdk-for-go/services/resources/mgmt/2018-06-01/subscriptions"
"github.com/jongio/azidext/go/azidext"
)
cred, err := azidentity.NewClientSecretCredential(tenantID, clientID, secret, nil)
handle(err)
client := subscriptions.NewClient()
client.Authorizer = azidext.NewTokenCredentialAdapter(cred, []string{"https://management.azure.com//.default"})
```
![Impressions](https://azure-sdk-impressions.azurewebsites.net/api/impressions/azure-sdk-for-go%2Fsdk%2Fazidentity%2FMIGRATION.png)

View file

@ -0,0 +1,243 @@
# Azure Identity Client Module for Go
The Azure Identity module provides Azure Active Directory (Azure AD) token authentication support across the Azure SDK. It includes a set of `TokenCredential` implementations, which can be used with Azure SDK clients supporting token authentication.
[![PkgGoDev](https://pkg.go.dev/badge/github.com/Azure/azure-sdk-for-go/sdk/azidentity)](https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/azidentity)
| [Azure Active Directory documentation](https://docs.microsoft.com/azure/active-directory/)
| [Source code](https://github.com/Azure/azure-sdk-for-go/tree/main/sdk/azidentity)
# Getting started
## Install the module
This project uses [Go modules](https://github.com/golang/go/wiki/Modules) for versioning and dependency management.
Install the Azure Identity module:
```sh
go get -u github.com/Azure/azure-sdk-for-go/sdk/azidentity
```
## Prerequisites
- an [Azure subscription](https://azure.microsoft.com/free/)
- Go 1.18
### Authenticating during local development
When debugging and executing code locally, developers typically use their own accounts to authenticate calls to Azure services. The `azidentity` module supports authenticating through developer tools to simplify local development.
#### Authenticating via the Azure CLI
`DefaultAzureCredential` and `AzureCLICredential` can authenticate as the user
signed in to the [Azure CLI](https://docs.microsoft.com/cli/azure). To sign in to the Azure CLI, run `az login`. On a system with a default web browser, the Azure CLI will launch the browser to authenticate a user.
When no default browser is available, `az login` will use the device code
authentication flow. This can also be selected manually by running `az login --use-device-code`.
## Key concepts
### Credentials
A credential is a type which contains or can obtain the data needed for a
service client to authenticate requests. Service clients across the Azure SDK
accept a credential instance when they are constructed, and use that credential
to authenticate requests.
The `azidentity` module focuses on OAuth authentication with Azure Active
Directory (AAD). It offers a variety of credential types capable of acquiring
an Azure AD access token. See [Credential Types](#credential-types "Credential Types") for a list of this module's credential types.
### DefaultAzureCredential
`DefaultAzureCredential` is appropriate for most apps that will be deployed to Azure. It combines common production credentials with development credentials. It attempts to authenticate via the following mechanisms in this order, stopping when one succeeds:
![DefaultAzureCredential authentication flow](img/mermaidjs/DefaultAzureCredentialAuthFlow.svg)
1. **Environment** - `DefaultAzureCredential` will read account information specified via [environment variables](#environment-variables) and use it to authenticate.
1. **Workload Identity** - If the app is deployed on Kubernetes with environment variables set by the workload identity webhook, `DefaultAzureCredential` will authenticate the configured identity.
1. **Managed Identity** - If the app is deployed to an Azure host with managed identity enabled, `DefaultAzureCredential` will authenticate with it.
1. **Azure CLI** - If a user or service principal has authenticated via the Azure CLI `az login` command, `DefaultAzureCredential` will authenticate that identity.
> Note: `DefaultAzureCredential` is intended to simplify getting started with the SDK by handling common scenarios with reasonable default behaviors. Developers who want more control or whose scenario isn't served by the default settings should use other credential types.
## Managed Identity
`DefaultAzureCredential` and `ManagedIdentityCredential` support
[managed identity authentication](https://docs.microsoft.com/azure/active-directory/managed-identities-azure-resources/overview)
in any hosting environment which supports managed identities, such as (this list is not exhaustive):
* [Azure App Service](https://docs.microsoft.com/azure/app-service/overview-managed-identity)
* [Azure Arc](https://docs.microsoft.com/azure/azure-arc/servers/managed-identity-authentication)
* [Azure Cloud Shell](https://docs.microsoft.com/azure/cloud-shell/msi-authorization)
* [Azure Kubernetes Service](https://docs.microsoft.com/azure/aks/use-managed-identity)
* [Azure Service Fabric](https://docs.microsoft.com/azure/service-fabric/concepts-managed-identity)
* [Azure Virtual Machines](https://docs.microsoft.com/azure/active-directory/managed-identities-azure-resources/how-to-use-vm-token)
## Examples
- [Authenticate with DefaultAzureCredential](#authenticate-with-defaultazurecredential "Authenticate with DefaultAzureCredential")
- [Define a custom authentication flow with ChainedTokenCredential](#define-a-custom-authentication-flow-with-chainedtokencredential "Define a custom authentication flow with ChainedTokenCredential")
- [Specify a user-assigned managed identity for DefaultAzureCredential](#specify-a-user-assigned-managed-identity-for-defaultazurecredential)
### Authenticate with DefaultAzureCredential
This example demonstrates authenticating a client from the `armresources` module with `DefaultAzureCredential`.
```go
cred, err := azidentity.NewDefaultAzureCredential(nil)
if err != nil {
// handle error
}
client := armresources.NewResourceGroupsClient("subscription ID", cred, nil)
```
### Specify a user-assigned managed identity for DefaultAzureCredential
To configure `DefaultAzureCredential` to authenticate a user-assigned managed identity, set the environment variable `AZURE_CLIENT_ID` to the identity's client ID.
### Define a custom authentication flow with `ChainedTokenCredential`
`DefaultAzureCredential` is generally the quickest way to get started developing apps for Azure. For more advanced scenarios, `ChainedTokenCredential` links multiple credential instances to be tried sequentially when authenticating. It will try each chained credential in turn until one provides a token or fails to authenticate due to an error.
The following example demonstrates creating a credential, which will attempt to authenticate using managed identity. It will fall back to authenticating via the Azure CLI when a managed identity is unavailable.
```go
managed, err := azidentity.NewManagedIdentityCredential(nil)
if err != nil {
// handle error
}
azCLI, err := azidentity.NewAzureCLICredential(nil)
if err != nil {
// handle error
}
chain, err := azidentity.NewChainedTokenCredential([]azcore.TokenCredential{managed, azCLI}, nil)
if err != nil {
// handle error
}
client := armresources.NewResourceGroupsClient("subscription ID", chain, nil)
```
## Credential Types
### Authenticating Azure Hosted Applications
|Credential|Usage
|-|-
|[DefaultAzureCredential](https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/azidentity#DefaultAzureCredential)|Simplified authentication experience for getting started developing Azure apps
|[ChainedTokenCredential](https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/azidentity#ChainedTokenCredential)|Define custom authentication flows, composing multiple credentials
|[EnvironmentCredential](https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/azidentity#EnvironmentCredential)|Authenticate a service principal or user configured by environment variables
|[ManagedIdentityCredential](https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/azidentity#ManagedIdentityCredential)|Authenticate the managed identity of an Azure resource
|[WorkloadIdentityCredential](https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/azidentity#WorkloadIdentityCredential)|Authenticate a workload identity on Kubernetes
### Authenticating Service Principals
|Credential|Usage
|-|-
|[ClientAssertionCredential](https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/azidentity#ClientAssertionCredential)|Authenticate a service principal with a signed client assertion
|[ClientCertificateCredential](https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/azidentity#ClientCertificateCredential)|Authenticate a service principal with a certificate
|[ClientSecretCredential](https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/azidentity#ClientSecretCredential)|Authenticate a service principal with a secret
### Authenticating Users
|Credential|Usage
|-|-
|[InteractiveBrowserCredential](https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/azidentity#InteractiveBrowserCredential)|Interactively authenticate a user with the default web browser
|[DeviceCodeCredential](https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/azidentity#DeviceCodeCredential)|Interactively authenticate a user on a device with limited UI
|[UsernamePasswordCredential](https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/azidentity#UsernamePasswordCredential)|Authenticate a user with a username and password
### Authenticating via Development Tools
|Credential|Usage
|-|-
|[AzureCLICredential](https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/azidentity#AzureCLICredential)|Authenticate as the user signed in to the Azure CLI
## Environment Variables
`DefaultAzureCredential` and `EnvironmentCredential` can be configured with environment variables. Each type of authentication requires values for specific variables:
#### Service principal with secret
|variable name|value
|-|-
|`AZURE_CLIENT_ID`|ID of an Azure Active Directory application
|`AZURE_TENANT_ID`|ID of the application's Azure Active Directory tenant
|`AZURE_CLIENT_SECRET`|one of the application's client secrets
#### Service principal with certificate
|variable name|value
|-|-
|`AZURE_CLIENT_ID`|ID of an Azure Active Directory application
|`AZURE_TENANT_ID`|ID of the application's Azure Active Directory tenant
|`AZURE_CLIENT_CERTIFICATE_PATH`|path to a certificate file including private key
|`AZURE_CLIENT_CERTIFICATE_PASSWORD`|password of the certificate file, if any
#### Username and password
|variable name|value
|-|-
|`AZURE_CLIENT_ID`|ID of an Azure Active Directory application
|`AZURE_USERNAME`|a username (usually an email address)
|`AZURE_PASSWORD`|that user's password
Configuration is attempted in the above order. For example, if values for a
client secret and certificate are both present, the client secret will be used.
## Troubleshooting
### Error Handling
Credentials return an `error` when they fail to authenticate or lack data they require to authenticate. For guidance on resolving errors from specific credential types, see the [troubleshooting guide](https://aka.ms/azsdk/go/identity/troubleshoot).
For more details on handling specific Azure Active Directory errors please refer to the
Azure Active Directory
[error code documentation](https://docs.microsoft.com/azure/active-directory/develop/reference-aadsts-error-codes).
### Logging
This module uses the classification-based logging implementation in `azcore`. To enable console logging for all SDK modules, set `AZURE_SDK_GO_LOGGING` to `all`. Use the `azcore/log` package to control log event output or to enable logs for `azidentity` only. For example:
```go
import azlog "github.com/Azure/azure-sdk-for-go/sdk/azcore/log"
// print log output to stdout
azlog.SetListener(func(event azlog.Event, s string) {
fmt.Println(s)
})
// include only azidentity credential logs
azlog.SetEvents(azidentity.EventAuthentication)
```
Credentials log basic information only, such as `GetToken` success or failure and errors. These log entries don't contain authentication secrets but may contain sensitive information.
## Next steps
Client and management modules listed on the [Azure SDK releases page](https://azure.github.io/azure-sdk/releases/latest/go.html) support authenticating with `azidentity` credential types. You can learn more about using these libraries in their documentation, which is linked from the release page.
## Provide Feedback
If you encounter bugs or have suggestions, please
[open an issue](https://github.com/Azure/azure-sdk-for-go/issues).
## Contributing
This project welcomes contributions and suggestions. Most contributions require
you to agree to a Contributor License Agreement (CLA) declaring that you have
the right to, and actually do, grant us the rights to use your contribution.
For details, visit [https://cla.microsoft.com](https://cla.microsoft.com).
When you submit a pull request, a CLA-bot will automatically determine whether
you need to provide a CLA and decorate the PR appropriately (e.g., label,
comment). Simply follow the instructions provided by the bot. You will only
need to do this once across all repos using our CLA.
This project has adopted the
[Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
For more information, see the
[Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/)
or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any
additional questions or comments.
![Impressions](https://azure-sdk-impressions.azurewebsites.net/api/impressions/azure-sdk-for-go%2Fsdk%2Fazidentity%2FREADME.png)

View file

@ -0,0 +1,205 @@
# Troubleshoot Azure Identity authentication issues
This troubleshooting guide covers failure investigation techniques, common errors for the credential types in the `azidentity` module, and mitigation steps to resolve these errors.
## Table of contents
- [Handle azidentity errors](#handle-azidentity-errors)
- [Permission issues](#permission-issues)
- [Find relevant information in errors](#find-relevant-information-in-errors)
- [Enable and configure logging](#enable-and-configure-logging)
- [Troubleshoot AzureCliCredential authentication issues](#troubleshoot-azureclicredential-authentication-issues)
- [Troubleshoot ClientCertificateCredential authentication issues](#troubleshoot-clientcertificatecredential-authentication-issues)
- [Troubleshoot ClientSecretCredential authentication issues](#troubleshoot-clientsecretcredential-authentication-issues)
- [Troubleshoot DefaultAzureCredential authentication issues](#troubleshoot-defaultazurecredential-authentication-issues)
- [Troubleshoot EnvironmentCredential authentication issues](#troubleshoot-environmentcredential-authentication-issues)
- [Troubleshoot ManagedIdentityCredential authentication issues](#troubleshoot-managedidentitycredential-authentication-issues)
- [Azure App Service and Azure Functions managed identity](#azure-app-service-and-azure-functions-managed-identity)
- [Azure Kubernetes Service managed identity](#azure-kubernetes-service-managed-identity)
- [Azure Virtual Machine managed identity](#azure-virtual-machine-managed-identity)
- [Troubleshoot UsernamePasswordCredential authentication issues](#troubleshoot-usernamepasswordcredential-authentication-issues)
- [Troubleshoot WorkloadIdentityCredential authentication issues](#troubleshoot-workloadidentitycredential-authentication-issues)
- [Get additional help](#get-additional-help)
## Handle azidentity errors
Any service client method that makes a request to the service may return an error due to authentication failure. This is because the credential authenticates on the first call to the service and on any subsequent call that needs to refresh an access token. Authentication errors include a description of the failure and possibly an error message from Azure Active Directory (Azure AD). Depending on the application, these errors may or may not be recoverable.
### Permission issues
Service client errors with a status code of 401 or 403 often indicate that authentication succeeded but the caller doesn't have permission to access the specified API. Check the service documentation to determine which RBAC roles are needed for the request, and ensure the authenticated user or service principal has the appropriate role assignments.
## Find relevant information in errors
Authentication errors can include responses from Azure AD and often contain information helpful in diagnosis. Consider the following error message:
```
ClientSecretCredential authentication failed
POST https://login.microsoftonline.com/3c631bb7-a9f7-4343-a5ba-a615913/oauth2/v2.0/token
--------------------------------------------------------------------------------
RESPONSE 401 Unauthorized
--------------------------------------------------------------------------------
{
"error": "invalid_client",
"error_description": "AADSTS7000215: Invalid client secret provided. Ensure the secret being sent in the request is the client secret value, not the client secret ID, for a secret added to app '86be4c01-505b-45e9-bfc0-9b825fd84'.\r\nTrace ID: 03da4b8e-5ffe-48ca-9754-aff4276f0100\r\nCorrelation ID: 7b12f9bb-2eef-42e3-ad75-eee69ec9088d\r\nTimestamp: 2022-03-02 18:25:26Z",
"error_codes": [
7000215
],
"timestamp": "2022-03-02 18:25:26Z",
"trace_id": "03da4b8e-5ffe-48ca-9754-aff4276f0100",
"correlation_id": "7b12f9bb-2eef-42e3-ad75-eee69ec9088d",
"error_uri": "https://login.microsoftonline.com/error?code=7000215"
}
--------------------------------------------------------------------------------
```
This error contains several pieces of information:
- __Failing Credential Type__: The type of credential that failed to authenticate. This can be helpful when diagnosing issues with chained credential types such as `DefaultAzureCredential` or `ChainedTokenCredential`.
- __Azure AD Error Code and Message__: The error code and message returned by Azure AD. This can give insight into the specific reason the request failed. For instance, in this case authentication failed because the provided client secret is incorrect. [Azure AD documentation](https://docs.microsoft.com/azure/active-directory/develop/reference-aadsts-error-codes#aadsts-error-codes) has more information on AADSTS error codes.
- __Correlation ID and Timestamp__: The correlation ID and timestamp identify the request in server-side logs. This information can be useful to support engineers diagnosing unexpected Azure AD failures.
### Enable and configure logging
`azidentity` provides the same logging capabilities as the rest of the Azure SDK. The simplest way to see the logs to help debug authentication issues is to print credential logs to the console.
```go
import azlog "github.com/Azure/azure-sdk-for-go/sdk/azcore/log"
// print log output to stdout
azlog.SetListener(func(event azlog.Event, s string) {
fmt.Println(s)
})
// include only azidentity credential logs
azlog.SetEvents(azidentity.EventAuthentication)
```
## Troubleshoot DefaultAzureCredential authentication issues
| Error |Description| Mitigation |
|---|---|---|
|"DefaultAzureCredential failed to acquire a token"|No credential in the `DefaultAzureCredential` chain provided a token|<ul><li>[Enable logging](#enable-and-configure-logging) to get further diagnostic information.</li><li>Consult the troubleshooting guide for underlying credential types for more information.</li><ul><li>[EnvironmentCredential](#troubleshoot-environmentcredential-authentication-issues)</li><li>[ManagedIdentityCredential](#troubleshoot-managedidentitycredential-authentication-issues)</li><li>[AzureCLICredential](#troubleshoot-azureclicredential-authentication-issues)</li></ul>|
|Error from the client with a status code of 401 or 403|Authentication succeeded but the authorizing Azure service responded with a 401 (Unauthorized), or 403 (Forbidden) status code|<ul><li>[Enable logging](#enable-and-configure-logging) to determine which credential in the chain returned the authenticating token.</li><li>If an unexpected credential is returning a token, check application configuration such as environment variables.</li><li>Ensure the correct role is assigned to the authenticated identity. For example, a service specific role rather than the subscription Owner role.</li></ul>|
## Troubleshoot EnvironmentCredential authentication issues
| Error Message |Description| Mitigation |
|---|---|---|
|Missing or incomplete environment variable configuration|A valid combination of environment variables wasn't set|Ensure the appropriate environment variables are set for the intended authentication method as described in the [module documentation](https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/azidentity#EnvironmentCredential)|
<a id="client-secret"></a>
## Troubleshoot ClientSecretCredential authentication issues
| Error Code | Issue | Mitigation |
|---|---|---|
|AADSTS7000215|An invalid client secret was provided.|Ensure the secret provided to the credential constructor is valid. If unsure, create a new client secret using the Azure portal. Details on creating a new client secret are in [Azure AD documentation](https://docs.microsoft.com/azure/active-directory/develop/howto-create-service-principal-portal#option-2-create-a-new-application-secret).|
|AADSTS7000222|An expired client secret was provided.|Create a new client secret using the Azure portal. Details on creating a new client secret are in [Azure AD documentation](https://docs.microsoft.com/azure/active-directory/develop/howto-create-service-principal-portal#option-2-create-a-new-application-secret).|
|AADSTS700016|The specified application wasn't found in the specified tenant.|Ensure the client and tenant IDs provided to the credential constructor are correct for your application registration. For multi-tenant apps, ensure the application has been added to the desired tenant by a tenant admin. To add a new application in the desired tenant, follow the [Azure AD instructions](https://docs.microsoft.com/azure/active-directory/develop/howto-create-service-principal-portal).|
<a id="client-cert"></a>
## Troubleshoot ClientCertificateCredential authentication issues
| Error Code | Description | Mitigation |
|---|---|---|
|AADSTS700027|Client assertion contains an invalid signature.|Ensure the specified certificate has been uploaded to the application registration as described in [Azure AD documentation](https://docs.microsoft.com/azure/active-directory/develop/howto-create-service-principal-portal#option-1-upload-a-certificate).|
|AADSTS700016|The specified application wasn't found in the specified tenant.|Ensure the client and tenant IDs provided to the credential constructor are correct for your application registration. For multi-tenant apps, ensure the application has been added to the desired tenant by a tenant admin. To add a new application in the desired tenant, follow the [Azure AD instructions](https://docs.microsoft.com/azure/active-directory/develop/howto-create-service-principal-portal).|
<a id="username-password"></a>
## Troubleshoot UsernamePasswordCredential authentication issues
| Error Code | Issue | Mitigation |
|---|---|---|
|AADSTS50126|The provided username or password is invalid.|Ensure the username and password provided to the credential constructor are valid.|
<a id="managed-id"></a>
## Troubleshoot ManagedIdentityCredential authentication issues
`ManagedIdentityCredential` is designed to work on a variety of Azure hosts support managed identity. Configuration and troubleshooting vary from host to host. The below table lists the Azure hosts that can be assigned a managed identity and are supported by `ManagedIdentityCredential`.
|Host Environment| | |
|---|---|---|
|Azure Virtual Machines and Scale Sets|[Configuration](https://docs.microsoft.com/azure/active-directory/managed-identities-azure-resources/qs-configure-portal-windows-vm)|[Troubleshooting](#azure-virtual-machine-managed-identity)|
|Azure App Service and Azure Functions|[Configuration](https://docs.microsoft.com/azure/app-service/overview-managed-identity)|[Troubleshooting](#azure-app-service-and-azure-functions-managed-identity)|
|Azure Kubernetes Service|[Configuration](https://azure.github.io/aad-pod-identity/docs/)|[Troubleshooting](#azure-kubernetes-service-managed-identity)|
|Azure Arc|[Configuration](https://docs.microsoft.com/azure/azure-arc/servers/managed-identity-authentication)||
|Azure Service Fabric|[Configuration](https://docs.microsoft.com/azure/service-fabric/concepts-managed-identity)||
### Azure Virtual Machine managed identity
| Error Message |Description| Mitigation |
|---|---|---|
|The requested identity hasnt been assigned to this resource.|The IMDS endpoint responded with a status code of 400, indicating the requested identity isnt assigned to the VM.|If using a user assigned identity, ensure the specified ID is correct.<p/><p/>If using a system assigned identity, make sure it has been enabled as described in [managed identity documentation](https://docs.microsoft.com/azure/active-directory/managed-identities-azure-resources/qs-configure-portal-windows-vm#enable-system-assigned-managed-identity-on-an-existing-vm).|
|The request failed due to a gateway error.|The request to the IMDS endpoint failed due to a gateway error, 502 or 504 status code.|IMDS doesn't support requests via proxy or gateway. Disable proxies or gateways running on the VM for requests to the IMDS endpoint `http://169.254.169.254`|
|No response received from the managed identity endpoint.|No response was received for the request to IMDS or the request timed out.|<ul><li>Ensure the VM is configured for managed identity as described in [managed identity documentation](https://docs.microsoft.com/azure/active-directory/managed-identities-azure-resources/qs-configure-portal-windows-vm).</li><li>Verify the IMDS endpoint is reachable on the VM. See [below](#verify-imds-is-available-on-the-vm) for instructions.</li></ul>|
|Multiple attempts failed to obtain a token from the managed identity endpoint.|The credential has exhausted its retries for a token request.|<ul><li>Refer to the error message for more details on specific failures.<li>Ensure the VM is configured for managed identity as described in [managed identity documentation](https://docs.microsoft.com/azure/active-directory/managed-identities-azure-resources/qs-configure-portal-windows-vm).</li><li>Verify the IMDS endpoint is reachable on the VM. See [below](#verify-imds-is-available-on-the-vm) for instructions.</li></ul>|
#### Verify IMDS is available on the VM
If you have access to the VM, you can use `curl` to verify the managed identity endpoint is available.
```sh
curl 'http://169.254.169.254/metadata/identity/oauth2/token?resource=https://management.core.windows.net&api-version=2018-02-01' -H "Metadata: true"
```
> This command's output will contain an access token and SHOULD NOT BE SHARED, to avoid compromising account security.
### Azure App Service and Azure Functions managed identity
| Error Message |Description| Mitigation |
|---|---|---|
|Get "`http://169.254.169.254/...`" i/o timeout|The App Service host hasn't set environment variables for managed identity configuration.|<ul><li>Ensure the App Service is configured for managed identity as described in [App Service documentation](https://docs.microsoft.com/azure/app-service/overview-managed-identity).</li><li>Verify the App Service environment is properly configured and the managed identity endpoint is available. See [below](#verify-the-app-service-managed-identity-endpoint-is-available) for instructions.</li></ul>|
#### Verify the App Service managed identity endpoint is available
If you can SSH into the App Service, you can verify managed identity is available in the environment. First ensure the environment variables `IDENTITY_ENDPOINT` and `IDENTITY_SECRET` are set. Then you can verify the managed identity endpoint is available using `curl`.
```sh
curl "$IDENTITY_ENDPOINT?resource=https://management.core.windows.net&api-version=2019-08-01" -H "X-IDENTITY-HEADER: $IDENTITY_HEADER"
```
> This command's output will contain an access token and SHOULD NOT BE SHARED, to avoid compromising account security.
### Azure Kubernetes Service managed identity
#### Pod Identity
| Error Message |Description| Mitigation |
|---|---|---|
|"no azure identity found for request clientID"|The application attempted to authenticate before an identity was assigned to its pod|Verify the pod is labeled correctly. This also occurs when a correctly labeled pod authenticates before the identity is ready. To prevent initialization races, configure NMI to set the Retry-After header in its responses as described in [Pod Identity documentation](https://azure.github.io/aad-pod-identity/docs/configure/feature_flags/#set-retry-after-header-in-nmi-response).
<a id="azure-cli"></a>
## Troubleshoot AzureCliCredential authentication issues
| Error Message |Description| Mitigation |
|---|---|---|
|Azure CLI not found on path|The Azure CLI isnt installed or isn't on the application's path.|<ul><li>Ensure the Azure CLI is installed as described in [Azure CLI documentation](https://docs.microsoft.com/cli/azure/install-azure-cli).</li><li>Validate the installation location is in the application's `PATH` environment variable.</li></ul>|
|Please run 'az login' to set up account|No account is currently logged into the Azure CLI, or the login has expired.|<ul><li>Run `az login` to log into the Azure CLI. More information about Azure CLI authentication is available in the [Azure CLI documentation](https://docs.microsoft.com/cli/azure/authenticate-azure-cli).</li><li>Verify that the Azure CLI can obtain tokens. See [below](#verify-the-azure-cli-can-obtain-tokens) for instructions.</li></ul>|
#### Verify the Azure CLI can obtain tokens
You can manually verify that the Azure CLI can authenticate and obtain tokens. First, use the `account` command to verify the logged in account.
```azurecli
az account show
```
Once you've verified the Azure CLI is using the correct account, you can validate that it's able to obtain tokens for that account.
```azurecli
az account get-access-token --output json --resource https://management.core.windows.net
```
> This command's output will contain an access token and SHOULD NOT BE SHARED, to avoid compromising account security.
<a id="workload"></a>
## Troubleshoot `WorkloadIdentityCredential` authentication issues
| Error Message |Description| Mitigation |
|---|---|---|
|no client ID/tenant ID/token file specified|Incomplete configuration|In most cases these values are provided via environment variables set by Azure Workload Identity.<ul><li>If your application runs on Azure Kubernetes Servide (AKS) or a cluster that has deployed the Azure Workload Identity admission webhook, check pod labels and service account configuration. See the [AKS documentation](https://learn.microsoft.com/azure/aks/workload-identity-deploy-cluster#disable-workload-identity) and [Azure Workload Identity troubleshooting guide](https://azure.github.io/azure-workload-identity/docs/troubleshooting.html) for more details.<li>If your application isn't running on AKS or your cluster hasn't deployed the Workload Identity admission webhook, set these values in `WorkloadIdentityCredentialOptions`
## Get additional help
Additional information on ways to reach out for support can be found in [SUPPORT.md](https://github.com/Azure/azure-sdk-for-go/blob/main/SUPPORT.md).

View file

@ -0,0 +1,6 @@
{
"AssetsRepo": "Azure/azure-sdk-assets",
"AssetsRepoPrefixPath": "go",
"TagPrefix": "go/azidentity",
"Tag": "go/azidentity_6225ab0470"
}

View file

@ -0,0 +1,190 @@
//go:build go1.18
// +build go1.18
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package azidentity
import (
"bytes"
"context"
"errors"
"io"
"net/http"
"net/url"
"os"
"regexp"
"strings"
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/cloud"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/streaming"
"github.com/AzureAD/microsoft-authentication-library-for-go/apps/confidential"
"github.com/AzureAD/microsoft-authentication-library-for-go/apps/public"
)
const (
azureAdditionallyAllowedTenants = "AZURE_ADDITIONALLY_ALLOWED_TENANTS"
azureAuthorityHost = "AZURE_AUTHORITY_HOST"
azureClientCertificatePassword = "AZURE_CLIENT_CERTIFICATE_PASSWORD"
azureClientCertificatePath = "AZURE_CLIENT_CERTIFICATE_PATH"
azureClientID = "AZURE_CLIENT_ID"
azureClientSecret = "AZURE_CLIENT_SECRET"
azureFederatedTokenFile = "AZURE_FEDERATED_TOKEN_FILE"
azurePassword = "AZURE_PASSWORD"
azureRegionalAuthorityName = "AZURE_REGIONAL_AUTHORITY_NAME"
azureTenantID = "AZURE_TENANT_ID"
azureUsername = "AZURE_USERNAME"
organizationsTenantID = "organizations"
developerSignOnClientID = "04b07795-8ddb-461a-bbee-02f9e1bf7b46"
defaultSuffix = "/.default"
tenantIDValidationErr = "invalid tenantID. You can locate your tenantID by following the instructions listed here: https://docs.microsoft.com/partner-center/find-ids-and-domain-names"
)
var (
// capability CP1 indicates the client application is capable of handling CAE claims challenges
cp1 = []string{"CP1"}
// CP1 is disabled until CAE support is added back
disableCP1 = true
)
var getConfidentialClient = func(clientID, tenantID string, cred confidential.Credential, co *azcore.ClientOptions, additionalOpts ...confidential.Option) (confidentialClient, error) {
if !validTenantID(tenantID) {
return confidential.Client{}, errors.New(tenantIDValidationErr)
}
authorityHost, err := setAuthorityHost(co.Cloud)
if err != nil {
return confidential.Client{}, err
}
authority := runtime.JoinPaths(authorityHost, tenantID)
o := []confidential.Option{
confidential.WithAzureRegion(os.Getenv(azureRegionalAuthorityName)),
confidential.WithHTTPClient(newPipelineAdapter(co)),
}
if !disableCP1 {
o = append(o, confidential.WithClientCapabilities(cp1))
}
o = append(o, additionalOpts...)
if strings.ToLower(tenantID) == "adfs" {
o = append(o, confidential.WithInstanceDiscovery(false))
}
return confidential.New(authority, clientID, cred, o...)
}
var getPublicClient = func(clientID, tenantID string, co *azcore.ClientOptions, additionalOpts ...public.Option) (public.Client, error) {
if !validTenantID(tenantID) {
return public.Client{}, errors.New(tenantIDValidationErr)
}
authorityHost, err := setAuthorityHost(co.Cloud)
if err != nil {
return public.Client{}, err
}
o := []public.Option{
public.WithAuthority(runtime.JoinPaths(authorityHost, tenantID)),
public.WithHTTPClient(newPipelineAdapter(co)),
}
if !disableCP1 {
o = append(o, public.WithClientCapabilities(cp1))
}
o = append(o, additionalOpts...)
if strings.ToLower(tenantID) == "adfs" {
o = append(o, public.WithInstanceDiscovery(false))
}
return public.New(clientID, o...)
}
// setAuthorityHost initializes the authority host for credentials. Precedence is:
// 1. cloud.Configuration.ActiveDirectoryAuthorityHost value set by user
// 2. value of AZURE_AUTHORITY_HOST
// 3. default: Azure Public Cloud
func setAuthorityHost(cc cloud.Configuration) (string, error) {
host := cc.ActiveDirectoryAuthorityHost
if host == "" {
if len(cc.Services) > 0 {
return "", errors.New("missing ActiveDirectoryAuthorityHost for specified cloud")
}
host = cloud.AzurePublic.ActiveDirectoryAuthorityHost
if envAuthorityHost := os.Getenv(azureAuthorityHost); envAuthorityHost != "" {
host = envAuthorityHost
}
}
u, err := url.Parse(host)
if err != nil {
return "", err
}
if u.Scheme != "https" {
return "", errors.New("cannot use an authority host without https")
}
return host, nil
}
// validTenantID return true is it receives a valid tenantID, returns false otherwise
func validTenantID(tenantID string) bool {
match, err := regexp.MatchString("^[0-9a-zA-Z-.]+$", tenantID)
if err != nil {
return false
}
return match
}
func newPipelineAdapter(opts *azcore.ClientOptions) pipelineAdapter {
pl := runtime.NewPipeline(component, version, runtime.PipelineOptions{}, opts)
return pipelineAdapter{pl: pl}
}
type pipelineAdapter struct {
pl runtime.Pipeline
}
func (p pipelineAdapter) CloseIdleConnections() {
// do nothing
}
func (p pipelineAdapter) Do(r *http.Request) (*http.Response, error) {
req, err := runtime.NewRequest(r.Context(), r.Method, r.URL.String())
if err != nil {
return nil, err
}
if r.Body != nil && r.Body != http.NoBody {
// create a rewindable body from the existing body as required
var body io.ReadSeekCloser
if rsc, ok := r.Body.(io.ReadSeekCloser); ok {
body = rsc
} else {
b, err := io.ReadAll(r.Body)
if err != nil {
return nil, err
}
body = streaming.NopCloser(bytes.NewReader(b))
}
err = req.SetBody(body, r.Header.Get("Content-Type"))
if err != nil {
return nil, err
}
}
resp, err := p.pl.Do(req)
if err != nil {
return nil, err
}
return resp, err
}
// enables fakes for test scenarios
type confidentialClient interface {
AcquireTokenSilent(ctx context.Context, scopes []string, options ...confidential.AcquireSilentOption) (confidential.AuthResult, error)
AcquireTokenByAuthCode(ctx context.Context, code string, redirectURI string, scopes []string, options ...confidential.AcquireByAuthCodeOption) (confidential.AuthResult, error)
AcquireTokenByCredential(ctx context.Context, scopes []string, options ...confidential.AcquireByCredentialOption) (confidential.AuthResult, error)
AcquireTokenOnBehalfOf(ctx context.Context, userAssertion string, scopes []string, options ...confidential.AcquireOnBehalfOfOption) (confidential.AuthResult, error)
}
// enables fakes for test scenarios
type publicClient interface {
AcquireTokenSilent(ctx context.Context, scopes []string, options ...public.AcquireSilentOption) (public.AuthResult, error)
AcquireTokenByUsernamePassword(ctx context.Context, scopes []string, username string, password string, options ...public.AcquireByUsernamePasswordOption) (public.AuthResult, error)
AcquireTokenByDeviceCode(ctx context.Context, scopes []string, options ...public.AcquireByDeviceCodeOption) (public.DeviceCode, error)
AcquireTokenByAuthCode(ctx context.Context, code string, redirectURI string, scopes []string, options ...public.AcquireByAuthCodeOption) (public.AuthResult, error)
AcquireTokenInteractive(ctx context.Context, scopes []string, options ...public.AcquireInteractiveOption) (public.AuthResult, error)
}

View file

@ -0,0 +1,180 @@
//go:build go1.18
// +build go1.18
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package azidentity
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"os"
"os/exec"
"regexp"
"runtime"
"strings"
"time"
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
)
const (
credNameAzureCLI = "AzureCLICredential"
timeoutCLIRequest = 10 * time.Second
)
// used by tests to fake invoking the CLI
type azureCLITokenProvider func(ctx context.Context, resource string, tenantID string) ([]byte, error)
// AzureCLICredentialOptions contains optional parameters for AzureCLICredential.
type AzureCLICredentialOptions struct {
// AdditionallyAllowedTenants specifies tenants for which the credential may acquire tokens, in addition
// to TenantID. Add the wildcard value "*" to allow the credential to acquire tokens for any tenant the
// logged in account can access.
AdditionallyAllowedTenants []string
// TenantID identifies the tenant the credential should authenticate in.
// Defaults to the CLI's default tenant, which is typically the home tenant of the logged in user.
TenantID string
tokenProvider azureCLITokenProvider
}
// init returns an instance of AzureCLICredentialOptions initialized with default values.
func (o *AzureCLICredentialOptions) init() {
if o.tokenProvider == nil {
o.tokenProvider = defaultTokenProvider()
}
}
// AzureCLICredential authenticates as the identity logged in to the Azure CLI.
type AzureCLICredential struct {
s *syncer
tokenProvider azureCLITokenProvider
}
// NewAzureCLICredential constructs an AzureCLICredential. Pass nil to accept default options.
func NewAzureCLICredential(options *AzureCLICredentialOptions) (*AzureCLICredential, error) {
cp := AzureCLICredentialOptions{}
if options != nil {
cp = *options
}
cp.init()
c := AzureCLICredential{tokenProvider: cp.tokenProvider}
c.s = newSyncer(credNameAzureCLI, cp.TenantID, cp.AdditionallyAllowedTenants, c.requestToken, c.requestToken)
return &c, nil
}
// GetToken requests a token from the Azure CLI. This credential doesn't cache tokens, so every call invokes the CLI.
// This method is called automatically by Azure SDK clients.
func (c *AzureCLICredential) GetToken(ctx context.Context, opts policy.TokenRequestOptions) (azcore.AccessToken, error) {
if len(opts.Scopes) != 1 {
return azcore.AccessToken{}, errors.New(credNameAzureCLI + ": GetToken() requires exactly one scope")
}
// CLI expects an AAD v1 resource, not a v2 scope
opts.Scopes = []string{strings.TrimSuffix(opts.Scopes[0], defaultSuffix)}
return c.s.GetToken(ctx, opts)
}
func (c *AzureCLICredential) requestToken(ctx context.Context, opts policy.TokenRequestOptions) (azcore.AccessToken, error) {
b, err := c.tokenProvider(ctx, opts.Scopes[0], opts.TenantID)
if err != nil {
return azcore.AccessToken{}, err
}
at, err := c.createAccessToken(b)
if err != nil {
return azcore.AccessToken{}, err
}
return at, nil
}
func defaultTokenProvider() func(ctx context.Context, resource string, tenantID string) ([]byte, error) {
return func(ctx context.Context, resource string, tenantID string) ([]byte, error) {
match, err := regexp.MatchString("^[0-9a-zA-Z-.:/]+$", resource)
if err != nil {
return nil, err
}
if !match {
return nil, fmt.Errorf(`%s: unexpected scope "%s". Only alphanumeric characters and ".", ";", "-", and "/" are allowed`, credNameAzureCLI, resource)
}
// set a default timeout for this authentication iff the application hasn't done so already
var cancel context.CancelFunc
if _, hasDeadline := ctx.Deadline(); !hasDeadline {
ctx, cancel = context.WithTimeout(ctx, timeoutCLIRequest)
defer cancel()
}
commandLine := "az account get-access-token -o json --resource " + resource
if tenantID != "" {
commandLine += " --tenant " + tenantID
}
var cliCmd *exec.Cmd
if runtime.GOOS == "windows" {
dir := os.Getenv("SYSTEMROOT")
if dir == "" {
return nil, newCredentialUnavailableError(credNameAzureCLI, "environment variable 'SYSTEMROOT' has no value")
}
cliCmd = exec.CommandContext(ctx, "cmd.exe", "/c", commandLine)
cliCmd.Dir = dir
} else {
cliCmd = exec.CommandContext(ctx, "/bin/sh", "-c", commandLine)
cliCmd.Dir = "/bin"
}
cliCmd.Env = os.Environ()
var stderr bytes.Buffer
cliCmd.Stderr = &stderr
output, err := cliCmd.Output()
if err != nil {
msg := stderr.String()
var exErr *exec.ExitError
if errors.As(err, &exErr) && exErr.ExitCode() == 127 || strings.HasPrefix(msg, "'az' is not recognized") {
msg = "Azure CLI not found on path"
}
if msg == "" {
msg = err.Error()
}
return nil, newCredentialUnavailableError(credNameAzureCLI, msg)
}
return output, nil
}
}
func (c *AzureCLICredential) createAccessToken(tk []byte) (azcore.AccessToken, error) {
t := struct {
AccessToken string `json:"accessToken"`
Authority string `json:"_authority"`
ClientID string `json:"_clientId"`
ExpiresOn string `json:"expiresOn"`
IdentityProvider string `json:"identityProvider"`
IsMRRT bool `json:"isMRRT"`
RefreshToken string `json:"refreshToken"`
Resource string `json:"resource"`
TokenType string `json:"tokenType"`
UserID string `json:"userId"`
}{}
err := json.Unmarshal(tk, &t)
if err != nil {
return azcore.AccessToken{}, err
}
// the Azure CLI's "expiresOn" is local time
exp, err := time.ParseInLocation("2006-01-02 15:04:05.999999", t.ExpiresOn, time.Local)
if err != nil {
return azcore.AccessToken{}, fmt.Errorf("Error parsing token expiration time %q: %v", t.ExpiresOn, err)
}
converted := azcore.AccessToken{
Token: t.AccessToken,
ExpiresOn: exp.UTC(),
}
return converted, nil
}
var _ azcore.TokenCredential = (*AzureCLICredential)(nil)

View file

@ -0,0 +1,138 @@
//go:build go1.18
// +build go1.18
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package azidentity
import (
"context"
"errors"
"fmt"
"strings"
"sync"
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
"github.com/Azure/azure-sdk-for-go/sdk/internal/log"
)
// ChainedTokenCredentialOptions contains optional parameters for ChainedTokenCredential.
type ChainedTokenCredentialOptions struct {
// RetrySources configures how the credential uses its sources. When true, the credential always attempts to
// authenticate through each source in turn, stopping when one succeeds. When false, the credential authenticates
// only through this first successful source--it never again tries the sources which failed.
RetrySources bool
}
// ChainedTokenCredential links together multiple credentials and tries them sequentially when authenticating. By default,
// it tries all the credentials until one authenticates, after which it always uses that credential.
type ChainedTokenCredential struct {
cond *sync.Cond
iterating bool
name string
retrySources bool
sources []azcore.TokenCredential
successfulCredential azcore.TokenCredential
}
// NewChainedTokenCredential creates a ChainedTokenCredential. Pass nil for options to accept defaults.
func NewChainedTokenCredential(sources []azcore.TokenCredential, options *ChainedTokenCredentialOptions) (*ChainedTokenCredential, error) {
if len(sources) == 0 {
return nil, errors.New("sources must contain at least one TokenCredential")
}
for _, source := range sources {
if source == nil { // cannot have a nil credential in the chain or else the application will panic when GetToken() is called on nil
return nil, errors.New("sources cannot contain nil")
}
}
cp := make([]azcore.TokenCredential, len(sources))
copy(cp, sources)
if options == nil {
options = &ChainedTokenCredentialOptions{}
}
return &ChainedTokenCredential{
cond: sync.NewCond(&sync.Mutex{}),
name: "ChainedTokenCredential",
retrySources: options.RetrySources,
sources: cp,
}, nil
}
// GetToken calls GetToken on the chained credentials in turn, stopping when one returns a token.
// This method is called automatically by Azure SDK clients.
func (c *ChainedTokenCredential) GetToken(ctx context.Context, opts policy.TokenRequestOptions) (azcore.AccessToken, error) {
if !c.retrySources {
// ensure only one goroutine at a time iterates the sources and perhaps sets c.successfulCredential
c.cond.L.Lock()
for {
if c.successfulCredential != nil {
c.cond.L.Unlock()
return c.successfulCredential.GetToken(ctx, opts)
}
if !c.iterating {
c.iterating = true
// allow other goroutines to wait while this one iterates
c.cond.L.Unlock()
break
}
c.cond.Wait()
}
}
var (
err error
errs []error
successfulCredential azcore.TokenCredential
token azcore.AccessToken
unavailableErr *credentialUnavailableError
)
for _, cred := range c.sources {
token, err = cred.GetToken(ctx, opts)
if err == nil {
log.Writef(EventAuthentication, "%s authenticated with %s", c.name, extractCredentialName(cred))
successfulCredential = cred
break
}
errs = append(errs, err)
// continue to the next source iff this one returned credentialUnavailableError
if !errors.As(err, &unavailableErr) {
break
}
}
if c.iterating {
c.cond.L.Lock()
// this is nil when all credentials returned an error
c.successfulCredential = successfulCredential
c.iterating = false
c.cond.L.Unlock()
c.cond.Broadcast()
}
// err is the error returned by the last GetToken call. It will be nil when that call succeeds
if err != nil {
// return credentialUnavailableError iff all sources did so; return AuthenticationFailedError otherwise
msg := createChainedErrorMessage(errs)
if errors.As(err, &unavailableErr) {
err = newCredentialUnavailableError(c.name, msg)
} else {
res := getResponseFromError(err)
err = newAuthenticationFailedError(c.name, msg, res, err)
}
}
return token, err
}
func createChainedErrorMessage(errs []error) string {
msg := "failed to acquire a token.\nAttempted credentials:"
for _, err := range errs {
msg += fmt.Sprintf("\n\t%s", err.Error())
}
return msg
}
func extractCredentialName(credential azcore.TokenCredential) string {
return strings.TrimPrefix(fmt.Sprintf("%T", credential), "*azidentity.")
}
var _ azcore.TokenCredential = (*ChainedTokenCredential)(nil)

View file

@ -0,0 +1,47 @@
# NOTE: Please refer to https://aka.ms/azsdk/engsys/ci-yaml before editing this file.
trigger:
branches:
include:
- main
- feature/*
- hotfix/*
- release/*
paths:
include:
- sdk/azidentity/
pr:
branches:
include:
- main
- feature/*
- hotfix/*
- release/*
paths:
include:
- sdk/azidentity/
stages:
- template: /eng/pipelines/templates/jobs/archetype-sdk-client.yml
parameters:
RunLiveTests: true
ServiceDirectory: 'azidentity'
PreSteps:
- pwsh: |
[System.Convert]::FromBase64String($env:PFX_CONTENTS) | Set-Content -Path $(Agent.TempDirectory)/test.pfx -AsByteStream
Set-Content -Path $(Agent.TempDirectory)/test.pem -Value $env:PEM_CONTENTS
[System.Convert]::FromBase64String($env:SNI_CONTENTS) | Set-Content -Path $(Agent.TempDirectory)/testsni.pfx -AsByteStream
env:
PFX_CONTENTS: $(net-identity-spcert-pfx)
PEM_CONTENTS: $(net-identity-spcert-pem)
SNI_CONTENTS: $(net-identity-spcert-sni)
EnvVars:
AZURE_IDENTITY_TEST_TENANTID: $(net-identity-tenantid)
AZURE_IDENTITY_TEST_USERNAME: $(net-identity-username)
AZURE_IDENTITY_TEST_PASSWORD: $(net-identity-password)
IDENTITY_SP_TENANT_ID: $(net-identity-sp-tenantid)
IDENTITY_SP_CLIENT_ID: $(net-identity-sp-clientid)
IDENTITY_SP_CLIENT_SECRET: $(net-identity-sp-clientsecret)
IDENTITY_SP_CERT_PEM: $(Agent.TempDirectory)/test.pem
IDENTITY_SP_CERT_PFX: $(Agent.TempDirectory)/test.pfx
IDENTITY_SP_CERT_SNI: $(Agent.TempDirectory)/testsni.pfx

View file

@ -0,0 +1,83 @@
//go:build go1.18
// +build go1.18
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package azidentity
import (
"context"
"errors"
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
"github.com/AzureAD/microsoft-authentication-library-for-go/apps/confidential"
)
const credNameAssertion = "ClientAssertionCredential"
// ClientAssertionCredential authenticates an application with assertions provided by a callback function.
// This credential is for advanced scenarios. [ClientCertificateCredential] has a more convenient API for
// the most common assertion scenario, authenticating a service principal with a certificate. See
// [Azure AD documentation] for details of the assertion format.
//
// [Azure AD documentation]: https://docs.microsoft.com/azure/active-directory/develop/active-directory-certificate-credentials#assertion-format
type ClientAssertionCredential struct {
client confidentialClient
s *syncer
}
// ClientAssertionCredentialOptions contains optional parameters for ClientAssertionCredential.
type ClientAssertionCredentialOptions struct {
azcore.ClientOptions
// AdditionallyAllowedTenants specifies additional tenants for which the credential may acquire tokens.
// Add the wildcard value "*" to allow the credential to acquire tokens for any tenant in which the
// application is registered.
AdditionallyAllowedTenants []string
// DisableInstanceDiscovery should be set true only by applications authenticating in disconnected clouds, or
// private clouds such as Azure Stack. It determines whether the credential requests Azure AD instance metadata
// from https://login.microsoft.com before authenticating. Setting this to true will skip this request, making
// the application responsible for ensuring the configured authority is valid and trustworthy.
DisableInstanceDiscovery bool
}
// NewClientAssertionCredential constructs a ClientAssertionCredential. The getAssertion function must be thread safe. Pass nil for options to accept defaults.
func NewClientAssertionCredential(tenantID, clientID string, getAssertion func(context.Context) (string, error), options *ClientAssertionCredentialOptions) (*ClientAssertionCredential, error) {
if getAssertion == nil {
return nil, errors.New("getAssertion must be a function that returns assertions")
}
if options == nil {
options = &ClientAssertionCredentialOptions{}
}
cred := confidential.NewCredFromAssertionCallback(
func(ctx context.Context, _ confidential.AssertionRequestOptions) (string, error) {
return getAssertion(ctx)
},
)
c, err := getConfidentialClient(clientID, tenantID, cred, &options.ClientOptions, confidential.WithInstanceDiscovery(!options.DisableInstanceDiscovery))
if err != nil {
return nil, err
}
cac := ClientAssertionCredential{client: c}
cac.s = newSyncer(credNameAssertion, tenantID, options.AdditionallyAllowedTenants, cac.requestToken, cac.silentAuth)
return &cac, nil
}
// GetToken requests an access token from Azure Active Directory. This method is called automatically by Azure SDK clients.
func (c *ClientAssertionCredential) GetToken(ctx context.Context, opts policy.TokenRequestOptions) (azcore.AccessToken, error) {
return c.s.GetToken(ctx, opts)
}
func (c *ClientAssertionCredential) silentAuth(ctx context.Context, opts policy.TokenRequestOptions) (azcore.AccessToken, error) {
ar, err := c.client.AcquireTokenSilent(ctx, opts.Scopes, confidential.WithTenantID(opts.TenantID))
return azcore.AccessToken{Token: ar.AccessToken, ExpiresOn: ar.ExpiresOn.UTC()}, err
}
func (c *ClientAssertionCredential) requestToken(ctx context.Context, opts policy.TokenRequestOptions) (azcore.AccessToken, error) {
ar, err := c.client.AcquireTokenByCredential(ctx, opts.Scopes, confidential.WithTenantID(opts.TenantID))
return azcore.AccessToken{Token: ar.AccessToken, ExpiresOn: ar.ExpiresOn.UTC()}, err
}
var _ azcore.TokenCredential = (*ClientAssertionCredential)(nil)

View file

@ -0,0 +1,172 @@
//go:build go1.18
// +build go1.18
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package azidentity
import (
"context"
"crypto"
"crypto/x509"
"encoding/pem"
"errors"
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
"github.com/AzureAD/microsoft-authentication-library-for-go/apps/confidential"
"golang.org/x/crypto/pkcs12"
)
const credNameCert = "ClientCertificateCredential"
// ClientCertificateCredentialOptions contains optional parameters for ClientCertificateCredential.
type ClientCertificateCredentialOptions struct {
azcore.ClientOptions
// AdditionallyAllowedTenants specifies additional tenants for which the credential may acquire tokens.
// Add the wildcard value "*" to allow the credential to acquire tokens for any tenant in which the
// application is registered.
AdditionallyAllowedTenants []string
// DisableInstanceDiscovery should be set true only by applications authenticating in disconnected clouds, or
// private clouds such as Azure Stack. It determines whether the credential requests Azure AD instance metadata
// from https://login.microsoft.com before authenticating. Setting this to true will skip this request, making
// the application responsible for ensuring the configured authority is valid and trustworthy.
DisableInstanceDiscovery bool
// SendCertificateChain controls whether the credential sends the public certificate chain in the x5c
// header of each token request's JWT. This is required for Subject Name/Issuer (SNI) authentication.
// Defaults to False.
SendCertificateChain bool
}
// ClientCertificateCredential authenticates a service principal with a certificate.
type ClientCertificateCredential struct {
client confidentialClient
s *syncer
}
// NewClientCertificateCredential constructs a ClientCertificateCredential. Pass nil for options to accept defaults.
func NewClientCertificateCredential(tenantID string, clientID string, certs []*x509.Certificate, key crypto.PrivateKey, options *ClientCertificateCredentialOptions) (*ClientCertificateCredential, error) {
if len(certs) == 0 {
return nil, errors.New("at least one certificate is required")
}
if options == nil {
options = &ClientCertificateCredentialOptions{}
}
cred, err := confidential.NewCredFromCert(certs, key)
if err != nil {
return nil, err
}
var o []confidential.Option
if options.SendCertificateChain {
o = append(o, confidential.WithX5C())
}
o = append(o, confidential.WithInstanceDiscovery(!options.DisableInstanceDiscovery))
c, err := getConfidentialClient(clientID, tenantID, cred, &options.ClientOptions, o...)
if err != nil {
return nil, err
}
cc := ClientCertificateCredential{client: c}
cc.s = newSyncer(credNameCert, tenantID, options.AdditionallyAllowedTenants, cc.requestToken, cc.silentAuth)
return &cc, nil
}
// GetToken requests an access token from Azure Active Directory. This method is called automatically by Azure SDK clients.
func (c *ClientCertificateCredential) GetToken(ctx context.Context, opts policy.TokenRequestOptions) (azcore.AccessToken, error) {
return c.s.GetToken(ctx, opts)
}
func (c *ClientCertificateCredential) silentAuth(ctx context.Context, opts policy.TokenRequestOptions) (azcore.AccessToken, error) {
ar, err := c.client.AcquireTokenSilent(ctx, opts.Scopes, confidential.WithTenantID(opts.TenantID))
return azcore.AccessToken{Token: ar.AccessToken, ExpiresOn: ar.ExpiresOn.UTC()}, err
}
func (c *ClientCertificateCredential) requestToken(ctx context.Context, opts policy.TokenRequestOptions) (azcore.AccessToken, error) {
ar, err := c.client.AcquireTokenByCredential(ctx, opts.Scopes, confidential.WithTenantID(opts.TenantID))
return azcore.AccessToken{Token: ar.AccessToken, ExpiresOn: ar.ExpiresOn.UTC()}, err
}
// ParseCertificates loads certificates and a private key, in PEM or PKCS12 format, for use with NewClientCertificateCredential.
// Pass nil for password if the private key isn't encrypted. This function can't decrypt keys in PEM format.
func ParseCertificates(certData []byte, password []byte) ([]*x509.Certificate, crypto.PrivateKey, error) {
var blocks []*pem.Block
var err error
if len(password) == 0 {
blocks, err = loadPEMCert(certData)
}
if len(blocks) == 0 || err != nil {
blocks, err = loadPKCS12Cert(certData, string(password))
}
if err != nil {
return nil, nil, err
}
var certs []*x509.Certificate
var pk crypto.PrivateKey
for _, block := range blocks {
switch block.Type {
case "CERTIFICATE":
c, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return nil, nil, err
}
certs = append(certs, c)
case "PRIVATE KEY":
if pk != nil {
return nil, nil, errors.New("certData contains multiple private keys")
}
pk, err = x509.ParsePKCS8PrivateKey(block.Bytes)
if err != nil {
pk, err = x509.ParsePKCS1PrivateKey(block.Bytes)
}
if err != nil {
return nil, nil, err
}
case "RSA PRIVATE KEY":
if pk != nil {
return nil, nil, errors.New("certData contains multiple private keys")
}
pk, err = x509.ParsePKCS1PrivateKey(block.Bytes)
if err != nil {
return nil, nil, err
}
}
}
if len(certs) == 0 {
return nil, nil, errors.New("found no certificate")
}
if pk == nil {
return nil, nil, errors.New("found no private key")
}
return certs, pk, nil
}
func loadPEMCert(certData []byte) ([]*pem.Block, error) {
blocks := []*pem.Block{}
for {
var block *pem.Block
block, certData = pem.Decode(certData)
if block == nil {
break
}
blocks = append(blocks, block)
}
if len(blocks) == 0 {
return nil, errors.New("didn't find any PEM blocks")
}
return blocks, nil
}
func loadPKCS12Cert(certData []byte, password string) ([]*pem.Block, error) {
blocks, err := pkcs12.ToPEM(certData, password)
if err != nil {
return nil, err
}
if len(blocks) == 0 {
// not mentioning PKCS12 in this message because we end up here when certData is garbage
return nil, errors.New("didn't find any certificate content")
}
return blocks, err
}
var _ azcore.TokenCredential = (*ClientCertificateCredential)(nil)

View file

@ -0,0 +1,75 @@
//go:build go1.18
// +build go1.18
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package azidentity
import (
"context"
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
"github.com/AzureAD/microsoft-authentication-library-for-go/apps/confidential"
)
const credNameSecret = "ClientSecretCredential"
// ClientSecretCredentialOptions contains optional parameters for ClientSecretCredential.
type ClientSecretCredentialOptions struct {
azcore.ClientOptions
// AdditionallyAllowedTenants specifies additional tenants for which the credential may acquire tokens.
// Add the wildcard value "*" to allow the credential to acquire tokens for any tenant in which the
// application is registered.
AdditionallyAllowedTenants []string
// DisableInstanceDiscovery should be set true only by applications authenticating in disconnected clouds, or
// private clouds such as Azure Stack. It determines whether the credential requests Azure AD instance metadata
// from https://login.microsoft.com before authenticating. Setting this to true will skip this request, making
// the application responsible for ensuring the configured authority is valid and trustworthy.
DisableInstanceDiscovery bool
}
// ClientSecretCredential authenticates an application with a client secret.
type ClientSecretCredential struct {
client confidentialClient
s *syncer
}
// NewClientSecretCredential constructs a ClientSecretCredential. Pass nil for options to accept defaults.
func NewClientSecretCredential(tenantID string, clientID string, clientSecret string, options *ClientSecretCredentialOptions) (*ClientSecretCredential, error) {
if options == nil {
options = &ClientSecretCredentialOptions{}
}
cred, err := confidential.NewCredFromSecret(clientSecret)
if err != nil {
return nil, err
}
c, err := getConfidentialClient(
clientID, tenantID, cred, &options.ClientOptions, confidential.WithInstanceDiscovery(!options.DisableInstanceDiscovery),
)
if err != nil {
return nil, err
}
csc := ClientSecretCredential{client: c}
csc.s = newSyncer(credNameSecret, tenantID, options.AdditionallyAllowedTenants, csc.requestToken, csc.silentAuth)
return &csc, nil
}
// GetToken requests an access token from Azure Active Directory. This method is called automatically by Azure SDK clients.
func (c *ClientSecretCredential) GetToken(ctx context.Context, opts policy.TokenRequestOptions) (azcore.AccessToken, error) {
return c.s.GetToken(ctx, opts)
}
func (c *ClientSecretCredential) silentAuth(ctx context.Context, opts policy.TokenRequestOptions) (azcore.AccessToken, error) {
ar, err := c.client.AcquireTokenSilent(ctx, opts.Scopes, confidential.WithTenantID(opts.TenantID))
return azcore.AccessToken{Token: ar.AccessToken, ExpiresOn: ar.ExpiresOn.UTC()}, err
}
func (c *ClientSecretCredential) requestToken(ctx context.Context, opts policy.TokenRequestOptions) (azcore.AccessToken, error) {
ar, err := c.client.AcquireTokenByCredential(ctx, opts.Scopes, confidential.WithTenantID(opts.TenantID))
return azcore.AccessToken{Token: ar.AccessToken, ExpiresOn: ar.ExpiresOn.UTC()}, err
}
var _ azcore.TokenCredential = (*ClientSecretCredential)(nil)

View file

@ -0,0 +1,209 @@
//go:build go1.18
// +build go1.18
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package azidentity
import (
"context"
"errors"
"os"
"strings"
"time"
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
"github.com/Azure/azure-sdk-for-go/sdk/internal/log"
)
// DefaultAzureCredentialOptions contains optional parameters for DefaultAzureCredential.
// These options may not apply to all credentials in the chain.
type DefaultAzureCredentialOptions struct {
azcore.ClientOptions
// AdditionallyAllowedTenants specifies additional tenants for which the credential may acquire tokens. Add
// the wildcard value "*" to allow the credential to acquire tokens for any tenant. This value can also be
// set as a semicolon delimited list of tenants in the environment variable AZURE_ADDITIONALLY_ALLOWED_TENANTS.
AdditionallyAllowedTenants []string
// DisableInstanceDiscovery should be set true only by applications authenticating in disconnected clouds, or
// private clouds such as Azure Stack. It determines whether the credential requests Azure AD instance metadata
// from https://login.microsoft.com before authenticating. Setting this to true will skip this request, making
// the application responsible for ensuring the configured authority is valid and trustworthy.
DisableInstanceDiscovery bool
// TenantID identifies the tenant the Azure CLI should authenticate in.
// Defaults to the CLI's default tenant, which is typically the home tenant of the user logged in to the CLI.
TenantID string
}
// DefaultAzureCredential is a default credential chain for applications that will deploy to Azure.
// It combines credentials suitable for deployment with credentials suitable for local development.
// It attempts to authenticate with each of these credential types, in the following order, stopping
// when one provides a token:
//
// - [EnvironmentCredential]
// - [WorkloadIdentityCredential], if environment variable configuration is set by the Azure workload
// identity webhook. Use [WorkloadIdentityCredential] directly when not using the webhook or needing
// more control over its configuration.
// - [ManagedIdentityCredential]
// - [AzureCLICredential]
//
// Consult the documentation for these credential types for more information on how they authenticate.
// Once a credential has successfully authenticated, DefaultAzureCredential will use that credential for
// every subsequent authentication.
type DefaultAzureCredential struct {
chain *ChainedTokenCredential
}
// NewDefaultAzureCredential creates a DefaultAzureCredential. Pass nil for options to accept defaults.
func NewDefaultAzureCredential(options *DefaultAzureCredentialOptions) (*DefaultAzureCredential, error) {
var creds []azcore.TokenCredential
var errorMessages []string
if options == nil {
options = &DefaultAzureCredentialOptions{}
}
additionalTenants := options.AdditionallyAllowedTenants
if len(additionalTenants) == 0 {
if tenants := os.Getenv(azureAdditionallyAllowedTenants); tenants != "" {
additionalTenants = strings.Split(tenants, ";")
}
}
envCred, err := NewEnvironmentCredential(&EnvironmentCredentialOptions{
ClientOptions: options.ClientOptions,
DisableInstanceDiscovery: options.DisableInstanceDiscovery,
additionallyAllowedTenants: additionalTenants,
})
if err == nil {
creds = append(creds, envCred)
} else {
errorMessages = append(errorMessages, "EnvironmentCredential: "+err.Error())
creds = append(creds, &defaultCredentialErrorReporter{credType: "EnvironmentCredential", err: err})
}
// workload identity requires values for AZURE_AUTHORITY_HOST, AZURE_CLIENT_ID, AZURE_FEDERATED_TOKEN_FILE, AZURE_TENANT_ID
wic, err := NewWorkloadIdentityCredential(&WorkloadIdentityCredentialOptions{
AdditionallyAllowedTenants: additionalTenants,
ClientOptions: options.ClientOptions,
DisableInstanceDiscovery: options.DisableInstanceDiscovery,
})
if err == nil {
creds = append(creds, wic)
} else {
errorMessages = append(errorMessages, credNameWorkloadIdentity+": "+err.Error())
creds = append(creds, &defaultCredentialErrorReporter{credType: credNameWorkloadIdentity, err: err})
}
o := &ManagedIdentityCredentialOptions{ClientOptions: options.ClientOptions}
if ID, ok := os.LookupEnv(azureClientID); ok {
o.ID = ClientID(ID)
}
miCred, err := NewManagedIdentityCredential(o)
if err == nil {
creds = append(creds, &timeoutWrapper{mic: miCred, timeout: time.Second})
} else {
errorMessages = append(errorMessages, credNameManagedIdentity+": "+err.Error())
creds = append(creds, &defaultCredentialErrorReporter{credType: credNameManagedIdentity, err: err})
}
cliCred, err := NewAzureCLICredential(&AzureCLICredentialOptions{AdditionallyAllowedTenants: additionalTenants, TenantID: options.TenantID})
if err == nil {
creds = append(creds, cliCred)
} else {
errorMessages = append(errorMessages, credNameAzureCLI+": "+err.Error())
creds = append(creds, &defaultCredentialErrorReporter{credType: credNameAzureCLI, err: err})
}
err = defaultAzureCredentialConstructorErrorHandler(len(creds), errorMessages)
if err != nil {
return nil, err
}
chain, err := NewChainedTokenCredential(creds, nil)
if err != nil {
return nil, err
}
chain.name = "DefaultAzureCredential"
return &DefaultAzureCredential{chain: chain}, nil
}
// GetToken requests an access token from Azure Active Directory. This method is called automatically by Azure SDK clients.
func (c *DefaultAzureCredential) GetToken(ctx context.Context, opts policy.TokenRequestOptions) (azcore.AccessToken, error) {
return c.chain.GetToken(ctx, opts)
}
var _ azcore.TokenCredential = (*DefaultAzureCredential)(nil)
func defaultAzureCredentialConstructorErrorHandler(numberOfSuccessfulCredentials int, errorMessages []string) (err error) {
errorMessage := strings.Join(errorMessages, "\n\t")
if numberOfSuccessfulCredentials == 0 {
return errors.New(errorMessage)
}
if len(errorMessages) != 0 {
log.Writef(EventAuthentication, "NewDefaultAzureCredential failed to initialize some credentials:\n\t%s", errorMessage)
}
return nil
}
// defaultCredentialErrorReporter is a substitute for credentials that couldn't be constructed.
// Its GetToken method always returns a credentialUnavailableError having the same message as
// the error that prevented constructing the credential. This ensures the message is present
// in the error returned by ChainedTokenCredential.GetToken()
type defaultCredentialErrorReporter struct {
credType string
err error
}
func (d *defaultCredentialErrorReporter) GetToken(ctx context.Context, opts policy.TokenRequestOptions) (azcore.AccessToken, error) {
if _, ok := d.err.(*credentialUnavailableError); ok {
return azcore.AccessToken{}, d.err
}
return azcore.AccessToken{}, newCredentialUnavailableError(d.credType, d.err.Error())
}
var _ azcore.TokenCredential = (*defaultCredentialErrorReporter)(nil)
// timeoutWrapper prevents a potentially very long timeout when managed identity isn't available
type timeoutWrapper struct {
mic *ManagedIdentityCredential
// timeout applies to all auth attempts until one doesn't time out
timeout time.Duration
}
// GetToken wraps DefaultAzureCredential's initial managed identity auth attempt with a short timeout
// because managed identity may not be available and connecting to IMDS can take several minutes to time out.
func (w *timeoutWrapper) GetToken(ctx context.Context, opts policy.TokenRequestOptions) (azcore.AccessToken, error) {
var tk azcore.AccessToken
var err error
// no need to synchronize around this value because it's written only within ChainedTokenCredential's critical section
if w.timeout > 0 {
c, cancel := context.WithTimeout(ctx, w.timeout)
defer cancel()
tk, err = w.mic.GetToken(c, opts)
if isAuthFailedDueToContext(err) {
err = newCredentialUnavailableError(credNameManagedIdentity, "managed identity timed out")
} else {
// some managed identity implementation is available, so don't apply the timeout to future calls
w.timeout = 0
}
} else {
tk, err = w.mic.GetToken(ctx, opts)
}
return tk, err
}
// unwraps nested AuthenticationFailedErrors to get the root error
func isAuthFailedDueToContext(err error) bool {
for {
var authFailedErr *AuthenticationFailedError
if !errors.As(err, &authFailedErr) {
break
}
err = authFailedErr.err
}
return errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded)
}

View file

@ -0,0 +1,136 @@
//go:build go1.18
// +build go1.18
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package azidentity
import (
"context"
"fmt"
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
"github.com/AzureAD/microsoft-authentication-library-for-go/apps/public"
)
const credNameDeviceCode = "DeviceCodeCredential"
// DeviceCodeCredentialOptions contains optional parameters for DeviceCodeCredential.
type DeviceCodeCredentialOptions struct {
azcore.ClientOptions
// AdditionallyAllowedTenants specifies additional tenants for which the credential may acquire
// tokens. Add the wildcard value "*" to allow the credential to acquire tokens for any tenant.
AdditionallyAllowedTenants []string
// ClientID is the ID of the application users will authenticate to.
// Defaults to the ID of an Azure development application.
ClientID string
// DisableInstanceDiscovery should be set true only by applications authenticating in disconnected clouds, or
// private clouds such as Azure Stack. It determines whether the credential requests Azure AD instance metadata
// from https://login.microsoft.com before authenticating. Setting this to true will skip this request, making
// the application responsible for ensuring the configured authority is valid and trustworthy.
DisableInstanceDiscovery bool
// TenantID is the Azure Active Directory tenant the credential authenticates in. Defaults to the
// "organizations" tenant, which can authenticate work and school accounts. Required for single-tenant
// applications.
TenantID string
// UserPrompt controls how the credential presents authentication instructions. The credential calls
// this function with authentication details when it receives a device code. By default, the credential
// prints these details to stdout.
UserPrompt func(context.Context, DeviceCodeMessage) error
}
func (o *DeviceCodeCredentialOptions) init() {
if o.TenantID == "" {
o.TenantID = organizationsTenantID
}
if o.ClientID == "" {
o.ClientID = developerSignOnClientID
}
if o.UserPrompt == nil {
o.UserPrompt = func(ctx context.Context, dc DeviceCodeMessage) error {
fmt.Println(dc.Message)
return nil
}
}
}
// DeviceCodeMessage contains the information a user needs to complete authentication.
type DeviceCodeMessage struct {
// UserCode is the user code returned by the service.
UserCode string `json:"user_code"`
// VerificationURL is the URL at which the user must authenticate.
VerificationURL string `json:"verification_uri"`
// Message is user instruction from Azure Active Directory.
Message string `json:"message"`
}
// DeviceCodeCredential acquires tokens for a user via the device code flow, which has the
// user browse to an Azure Active Directory URL, enter a code, and authenticate. It's useful
// for authenticating a user in an environment without a web browser, such as an SSH session.
// If a web browser is available, InteractiveBrowserCredential is more convenient because it
// automatically opens a browser to the login page.
type DeviceCodeCredential struct {
account public.Account
client publicClient
s *syncer
prompt func(context.Context, DeviceCodeMessage) error
}
// NewDeviceCodeCredential creates a DeviceCodeCredential. Pass nil to accept default options.
func NewDeviceCodeCredential(options *DeviceCodeCredentialOptions) (*DeviceCodeCredential, error) {
cp := DeviceCodeCredentialOptions{}
if options != nil {
cp = *options
}
cp.init()
c, err := getPublicClient(
cp.ClientID, cp.TenantID, &cp.ClientOptions, public.WithInstanceDiscovery(!cp.DisableInstanceDiscovery),
)
if err != nil {
return nil, err
}
cred := DeviceCodeCredential{client: c, prompt: cp.UserPrompt}
cred.s = newSyncer(credNameDeviceCode, cp.TenantID, cp.AdditionallyAllowedTenants, cred.requestToken, cred.silentAuth)
return &cred, nil
}
// GetToken requests an access token from Azure Active Directory. It will begin the device code flow and poll until the user completes authentication.
// This method is called automatically by Azure SDK clients.
func (c *DeviceCodeCredential) GetToken(ctx context.Context, opts policy.TokenRequestOptions) (azcore.AccessToken, error) {
return c.s.GetToken(ctx, opts)
}
func (c *DeviceCodeCredential) requestToken(ctx context.Context, opts policy.TokenRequestOptions) (azcore.AccessToken, error) {
dc, err := c.client.AcquireTokenByDeviceCode(ctx, opts.Scopes, public.WithTenantID(opts.TenantID))
if err != nil {
return azcore.AccessToken{}, err
}
err = c.prompt(ctx, DeviceCodeMessage{
Message: dc.Result.Message,
UserCode: dc.Result.UserCode,
VerificationURL: dc.Result.VerificationURL,
})
if err != nil {
return azcore.AccessToken{}, err
}
ar, err := dc.AuthenticationResult(ctx)
if err != nil {
return azcore.AccessToken{}, err
}
c.account = ar.Account
return azcore.AccessToken{Token: ar.AccessToken, ExpiresOn: ar.ExpiresOn.UTC()}, err
}
func (c *DeviceCodeCredential) silentAuth(ctx context.Context, opts policy.TokenRequestOptions) (azcore.AccessToken, error) {
ar, err := c.client.AcquireTokenSilent(ctx, opts.Scopes,
public.WithSilentAccount(c.account),
public.WithTenantID(opts.TenantID),
)
return azcore.AccessToken{Token: ar.AccessToken, ExpiresOn: ar.ExpiresOn.UTC()}, err
}
var _ azcore.TokenCredential = (*DeviceCodeCredential)(nil)

View file

@ -0,0 +1,164 @@
//go:build go1.18
// +build go1.18
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package azidentity
import (
"context"
"errors"
"fmt"
"os"
"strings"
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
"github.com/Azure/azure-sdk-for-go/sdk/internal/log"
)
const envVarSendCertChain = "AZURE_CLIENT_SEND_CERTIFICATE_CHAIN"
// EnvironmentCredentialOptions contains optional parameters for EnvironmentCredential
type EnvironmentCredentialOptions struct {
azcore.ClientOptions
// DisableInstanceDiscovery should be set true only by applications authenticating in disconnected clouds, or
// private clouds such as Azure Stack. It determines whether the credential requests Azure AD instance metadata
// from https://login.microsoft.com before authenticating. Setting this to true will skip this request, making
// the application responsible for ensuring the configured authority is valid and trustworthy.
DisableInstanceDiscovery bool
// additionallyAllowedTenants is used only by NewDefaultAzureCredential() to enable that constructor's explicit
// option to override the value of AZURE_ADDITIONALLY_ALLOWED_TENANTS. Applications using EnvironmentCredential
// directly should set that variable instead. This field should remain unexported to preserve this credential's
// unambiguous "all configuration from environment variables" design.
additionallyAllowedTenants []string
}
// EnvironmentCredential authenticates a service principal with a secret or certificate, or a user with a password, depending
// on environment variable configuration. It reads configuration from these variables, in the following order:
//
// # Service principal with client secret
//
// AZURE_TENANT_ID: ID of the service principal's tenant. Also called its "directory" ID.
//
// AZURE_CLIENT_ID: the service principal's client ID
//
// AZURE_CLIENT_SECRET: one of the service principal's client secrets
//
// # Service principal with certificate
//
// AZURE_TENANT_ID: ID of the service principal's tenant. Also called its "directory" ID.
//
// AZURE_CLIENT_ID: the service principal's client ID
//
// AZURE_CLIENT_CERTIFICATE_PATH: path to a PEM or PKCS12 certificate file including the private key.
//
// AZURE_CLIENT_CERTIFICATE_PASSWORD: (optional) password for the certificate file.
//
// # User with username and password
//
// AZURE_TENANT_ID: (optional) tenant to authenticate in. Defaults to "organizations".
//
// AZURE_CLIENT_ID: client ID of the application the user will authenticate to
//
// AZURE_USERNAME: a username (usually an email address)
//
// AZURE_PASSWORD: the user's password
//
// # Configuration for multitenant applications
//
// To enable multitenant authentication, set AZURE_ADDITIONALLY_ALLOWED_TENANTS with a semicolon delimited list of tenants
// the credential may request tokens from in addition to the tenant specified by AZURE_TENANT_ID. Set
// AZURE_ADDITIONALLY_ALLOWED_TENANTS to "*" to enable the credential to request a token from any tenant.
type EnvironmentCredential struct {
cred azcore.TokenCredential
}
// NewEnvironmentCredential creates an EnvironmentCredential. Pass nil to accept default options.
func NewEnvironmentCredential(options *EnvironmentCredentialOptions) (*EnvironmentCredential, error) {
if options == nil {
options = &EnvironmentCredentialOptions{}
}
tenantID := os.Getenv(azureTenantID)
if tenantID == "" {
return nil, errors.New("missing environment variable AZURE_TENANT_ID")
}
clientID := os.Getenv(azureClientID)
if clientID == "" {
return nil, errors.New("missing environment variable " + azureClientID)
}
// tenants set by NewDefaultAzureCredential() override the value of AZURE_ADDITIONALLY_ALLOWED_TENANTS
additionalTenants := options.additionallyAllowedTenants
if len(additionalTenants) == 0 {
if tenants := os.Getenv(azureAdditionallyAllowedTenants); tenants != "" {
additionalTenants = strings.Split(tenants, ";")
}
}
if clientSecret := os.Getenv(azureClientSecret); clientSecret != "" {
log.Write(EventAuthentication, "EnvironmentCredential will authenticate with ClientSecretCredential")
o := &ClientSecretCredentialOptions{
AdditionallyAllowedTenants: additionalTenants,
ClientOptions: options.ClientOptions,
DisableInstanceDiscovery: options.DisableInstanceDiscovery,
}
cred, err := NewClientSecretCredential(tenantID, clientID, clientSecret, o)
if err != nil {
return nil, err
}
return &EnvironmentCredential{cred: cred}, nil
}
if certPath := os.Getenv(azureClientCertificatePath); certPath != "" {
log.Write(EventAuthentication, "EnvironmentCredential will authenticate with ClientCertificateCredential")
certData, err := os.ReadFile(certPath)
if err != nil {
return nil, fmt.Errorf(`failed to read certificate file "%s": %v`, certPath, err)
}
var password []byte
if v := os.Getenv(azureClientCertificatePassword); v != "" {
password = []byte(v)
}
certs, key, err := ParseCertificates(certData, password)
if err != nil {
return nil, fmt.Errorf(`failed to load certificate from "%s": %v`, certPath, err)
}
o := &ClientCertificateCredentialOptions{
AdditionallyAllowedTenants: additionalTenants,
ClientOptions: options.ClientOptions,
DisableInstanceDiscovery: options.DisableInstanceDiscovery,
}
if v, ok := os.LookupEnv(envVarSendCertChain); ok {
o.SendCertificateChain = v == "1" || strings.ToLower(v) == "true"
}
cred, err := NewClientCertificateCredential(tenantID, clientID, certs, key, o)
if err != nil {
return nil, err
}
return &EnvironmentCredential{cred: cred}, nil
}
if username := os.Getenv(azureUsername); username != "" {
if password := os.Getenv(azurePassword); password != "" {
log.Write(EventAuthentication, "EnvironmentCredential will authenticate with UsernamePasswordCredential")
o := &UsernamePasswordCredentialOptions{
AdditionallyAllowedTenants: additionalTenants,
ClientOptions: options.ClientOptions,
DisableInstanceDiscovery: options.DisableInstanceDiscovery,
}
cred, err := NewUsernamePasswordCredential(tenantID, clientID, username, password, o)
if err != nil {
return nil, err
}
return &EnvironmentCredential{cred: cred}, nil
}
return nil, errors.New("no value for AZURE_PASSWORD")
}
return nil, errors.New("incomplete environment variable configuration. Only AZURE_TENANT_ID and AZURE_CLIENT_ID are set")
}
// GetToken requests an access token from Azure Active Directory. This method is called automatically by Azure SDK clients.
func (c *EnvironmentCredential) GetToken(ctx context.Context, opts policy.TokenRequestOptions) (azcore.AccessToken, error) {
return c.cred.GetToken(ctx, opts)
}
var _ azcore.TokenCredential = (*EnvironmentCredential)(nil)

View file

@ -0,0 +1,129 @@
//go:build go1.18
// +build go1.18
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package azidentity
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"github.com/Azure/azure-sdk-for-go/sdk/internal/errorinfo"
msal "github.com/AzureAD/microsoft-authentication-library-for-go/apps/errors"
)
// getResponseFromError retrieves the response carried by
// an AuthenticationFailedError or MSAL CallErr, if any
func getResponseFromError(err error) *http.Response {
var a *AuthenticationFailedError
var c msal.CallErr
var res *http.Response
if errors.As(err, &c) {
res = c.Resp
} else if errors.As(err, &a) {
res = a.RawResponse
}
return res
}
// AuthenticationFailedError indicates an authentication request has failed.
type AuthenticationFailedError struct {
// RawResponse is the HTTP response motivating the error, if available.
RawResponse *http.Response
credType string
message string
err error
}
func newAuthenticationFailedError(credType string, message string, resp *http.Response, err error) error {
return &AuthenticationFailedError{credType: credType, message: message, RawResponse: resp, err: err}
}
// Error implements the error interface. Note that the message contents are not contractual and can change over time.
func (e *AuthenticationFailedError) Error() string {
if e.RawResponse == nil {
return e.credType + ": " + e.message
}
msg := &bytes.Buffer{}
fmt.Fprintf(msg, e.credType+" authentication failed\n")
fmt.Fprintf(msg, "%s %s://%s%s\n", e.RawResponse.Request.Method, e.RawResponse.Request.URL.Scheme, e.RawResponse.Request.URL.Host, e.RawResponse.Request.URL.Path)
fmt.Fprintln(msg, "--------------------------------------------------------------------------------")
fmt.Fprintf(msg, "RESPONSE %s\n", e.RawResponse.Status)
fmt.Fprintln(msg, "--------------------------------------------------------------------------------")
body, err := io.ReadAll(e.RawResponse.Body)
e.RawResponse.Body.Close()
if err != nil {
fmt.Fprintf(msg, "Error reading response body: %v", err)
} else if len(body) > 0 {
e.RawResponse.Body = io.NopCloser(bytes.NewReader(body))
if err := json.Indent(msg, body, "", " "); err != nil {
// failed to pretty-print so just dump it verbatim
fmt.Fprint(msg, string(body))
}
} else {
fmt.Fprint(msg, "Response contained no body")
}
fmt.Fprintln(msg, "\n--------------------------------------------------------------------------------")
var anchor string
switch e.credType {
case credNameAzureCLI:
anchor = "azure-cli"
case credNameCert:
anchor = "client-cert"
case credNameSecret:
anchor = "client-secret"
case credNameManagedIdentity:
anchor = "managed-id"
case credNameUserPassword:
anchor = "username-password"
case credNameWorkloadIdentity:
anchor = "workload"
}
if anchor != "" {
fmt.Fprintf(msg, "To troubleshoot, visit https://aka.ms/azsdk/go/identity/troubleshoot#%s", anchor)
}
return msg.String()
}
// NonRetriable indicates the request which provoked this error shouldn't be retried.
func (*AuthenticationFailedError) NonRetriable() {
// marker method
}
var _ errorinfo.NonRetriable = (*AuthenticationFailedError)(nil)
// credentialUnavailableError indicates a credential can't attempt authentication because it lacks required
// data or state
type credentialUnavailableError struct {
message string
}
// newCredentialUnavailableError is an internal helper that ensures consistent error message formatting
func newCredentialUnavailableError(credType, message string) error {
msg := fmt.Sprintf("%s: %s", credType, message)
return &credentialUnavailableError{msg}
}
// NewCredentialUnavailableError constructs an error indicating a credential can't attempt authentication
// because it lacks required data or state. When [ChainedTokenCredential] receives this error it will try
// its next credential, if any.
func NewCredentialUnavailableError(message string) error {
return &credentialUnavailableError{message}
}
// Error implements the error interface. Note that the message contents are not contractual and can change over time.
func (e *credentialUnavailableError) Error() string {
return e.message
}
// NonRetriable is a marker method indicating this error should not be retried. It has no implementation.
func (e *credentialUnavailableError) NonRetriable() {}
var _ errorinfo.NonRetriable = (*credentialUnavailableError)(nil)

View file

@ -0,0 +1,106 @@
//go:build go1.18
// +build go1.18
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package azidentity
import (
"context"
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
"github.com/AzureAD/microsoft-authentication-library-for-go/apps/public"
)
const credNameBrowser = "InteractiveBrowserCredential"
// InteractiveBrowserCredentialOptions contains optional parameters for InteractiveBrowserCredential.
type InteractiveBrowserCredentialOptions struct {
azcore.ClientOptions
// AdditionallyAllowedTenants specifies additional tenants for which the credential may acquire
// tokens. Add the wildcard value "*" to allow the credential to acquire tokens for any tenant.
AdditionallyAllowedTenants []string
// ClientID is the ID of the application users will authenticate to.
// Defaults to the ID of an Azure development application.
ClientID string
// DisableInstanceDiscovery should be set true only by applications authenticating in disconnected clouds, or
// private clouds such as Azure Stack. It determines whether the credential requests Azure AD instance metadata
// from https://login.microsoft.com before authenticating. Setting this to true will skip this request, making
// the application responsible for ensuring the configured authority is valid and trustworthy.
DisableInstanceDiscovery bool
// LoginHint pre-populates the account prompt with a username. Users may choose to authenticate a different account.
LoginHint string
// RedirectURL is the URL Azure Active Directory will redirect to with the access token. This is required
// only when setting ClientID, and must match a redirect URI in the application's registration.
// Applications which have registered "http://localhost" as a redirect URI need not set this option.
RedirectURL string
// TenantID is the Azure Active Directory tenant the credential authenticates in. Defaults to the
// "organizations" tenant, which can authenticate work and school accounts.
TenantID string
}
func (o *InteractiveBrowserCredentialOptions) init() {
if o.TenantID == "" {
o.TenantID = organizationsTenantID
}
if o.ClientID == "" {
o.ClientID = developerSignOnClientID
}
}
// InteractiveBrowserCredential opens a browser to interactively authenticate a user.
type InteractiveBrowserCredential struct {
account public.Account
client publicClient
options InteractiveBrowserCredentialOptions
s *syncer
}
// NewInteractiveBrowserCredential constructs a new InteractiveBrowserCredential. Pass nil to accept default options.
func NewInteractiveBrowserCredential(options *InteractiveBrowserCredentialOptions) (*InteractiveBrowserCredential, error) {
cp := InteractiveBrowserCredentialOptions{}
if options != nil {
cp = *options
}
cp.init()
c, err := getPublicClient(cp.ClientID, cp.TenantID, &cp.ClientOptions, public.WithInstanceDiscovery(!cp.DisableInstanceDiscovery))
if err != nil {
return nil, err
}
ibc := InteractiveBrowserCredential{client: c, options: cp}
ibc.s = newSyncer(credNameBrowser, cp.TenantID, cp.AdditionallyAllowedTenants, ibc.requestToken, ibc.silentAuth)
return &ibc, nil
}
// GetToken requests an access token from Azure Active Directory. This method is called automatically by Azure SDK clients.
func (c *InteractiveBrowserCredential) GetToken(ctx context.Context, opts policy.TokenRequestOptions) (azcore.AccessToken, error) {
return c.s.GetToken(ctx, opts)
}
func (c *InteractiveBrowserCredential) requestToken(ctx context.Context, opts policy.TokenRequestOptions) (azcore.AccessToken, error) {
ar, err := c.client.AcquireTokenInteractive(ctx, opts.Scopes,
public.WithLoginHint(c.options.LoginHint),
public.WithRedirectURI(c.options.RedirectURL),
public.WithTenantID(opts.TenantID),
)
if err == nil {
c.account = ar.Account
}
return azcore.AccessToken{Token: ar.AccessToken, ExpiresOn: ar.ExpiresOn.UTC()}, err
}
func (c *InteractiveBrowserCredential) silentAuth(ctx context.Context, opts policy.TokenRequestOptions) (azcore.AccessToken, error) {
ar, err := c.client.AcquireTokenSilent(ctx, opts.Scopes,
public.WithSilentAccount(c.account),
public.WithTenantID(opts.TenantID),
)
return azcore.AccessToken{Token: ar.AccessToken, ExpiresOn: ar.ExpiresOn.UTC()}, err
}
var _ azcore.TokenCredential = (*InteractiveBrowserCredential)(nil)

View file

@ -0,0 +1,14 @@
//go:build go1.18
// +build go1.18
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package azidentity
import "github.com/Azure/azure-sdk-for-go/sdk/internal/log"
// EventAuthentication entries contain information about authentication.
// This includes information like the names of environment variables
// used when obtaining credentials and the type of credential used.
const EventAuthentication log.Event = "Authentication"

View file

@ -0,0 +1,388 @@
//go:build go1.18
// +build go1.18
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package azidentity
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"net/url"
"os"
"strconv"
"strings"
"time"
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/streaming"
"github.com/Azure/azure-sdk-for-go/sdk/internal/log"
"github.com/AzureAD/microsoft-authentication-library-for-go/apps/confidential"
)
const (
arcIMDSEndpoint = "IMDS_ENDPOINT"
identityEndpoint = "IDENTITY_ENDPOINT"
identityHeader = "IDENTITY_HEADER"
identityServerThumbprint = "IDENTITY_SERVER_THUMBPRINT"
headerMetadata = "Metadata"
imdsEndpoint = "http://169.254.169.254/metadata/identity/oauth2/token"
msiEndpoint = "MSI_ENDPOINT"
imdsAPIVersion = "2018-02-01"
azureArcAPIVersion = "2019-08-15"
serviceFabricAPIVersion = "2019-07-01-preview"
qpClientID = "client_id"
qpResID = "mi_res_id"
)
type msiType int
const (
msiTypeAppService msiType = iota
msiTypeAzureArc
msiTypeCloudShell
msiTypeIMDS
msiTypeServiceFabric
)
// managedIdentityClient provides the base for authenticating in managed identity environments
// This type includes an runtime.Pipeline and TokenCredentialOptions.
type managedIdentityClient struct {
pipeline runtime.Pipeline
msiType msiType
endpoint string
id ManagedIDKind
}
type wrappedNumber json.Number
func (n *wrappedNumber) UnmarshalJSON(b []byte) error {
c := string(b)
if c == "\"\"" {
return nil
}
return json.Unmarshal(b, (*json.Number)(n))
}
// setIMDSRetryOptionDefaults sets zero-valued fields to default values appropriate for IMDS
func setIMDSRetryOptionDefaults(o *policy.RetryOptions) {
if o.MaxRetries == 0 {
o.MaxRetries = 5
}
if o.MaxRetryDelay == 0 {
o.MaxRetryDelay = 1 * time.Minute
}
if o.RetryDelay == 0 {
o.RetryDelay = 2 * time.Second
}
if o.StatusCodes == nil {
o.StatusCodes = []int{
// IMDS docs recommend retrying 404, 429 and all 5xx
// https://docs.microsoft.com/azure/active-directory/managed-identities-azure-resources/how-to-use-vm-token#error-handling
http.StatusNotFound, // 404
http.StatusTooManyRequests, // 429
http.StatusInternalServerError, // 500
http.StatusNotImplemented, // 501
http.StatusBadGateway, // 502
http.StatusGatewayTimeout, // 504
http.StatusHTTPVersionNotSupported, // 505
http.StatusVariantAlsoNegotiates, // 506
http.StatusInsufficientStorage, // 507
http.StatusLoopDetected, // 508
http.StatusNotExtended, // 510
http.StatusNetworkAuthenticationRequired, // 511
}
}
if o.TryTimeout == 0 {
o.TryTimeout = 1 * time.Minute
}
}
// newManagedIdentityClient creates a new instance of the ManagedIdentityClient with the ManagedIdentityCredentialOptions
// that are passed into it along with a default pipeline.
// options: ManagedIdentityCredentialOptions configure policies for the pipeline and the authority host that
// will be used to retrieve tokens and authenticate
func newManagedIdentityClient(options *ManagedIdentityCredentialOptions) (*managedIdentityClient, error) {
if options == nil {
options = &ManagedIdentityCredentialOptions{}
}
cp := options.ClientOptions
c := managedIdentityClient{id: options.ID, endpoint: imdsEndpoint, msiType: msiTypeIMDS}
env := "IMDS"
if endpoint, ok := os.LookupEnv(identityEndpoint); ok {
if _, ok := os.LookupEnv(identityHeader); ok {
if _, ok := os.LookupEnv(identityServerThumbprint); ok {
env = "Service Fabric"
c.endpoint = endpoint
c.msiType = msiTypeServiceFabric
} else {
env = "App Service"
c.endpoint = endpoint
c.msiType = msiTypeAppService
}
} else if _, ok := os.LookupEnv(arcIMDSEndpoint); ok {
env = "Azure Arc"
c.endpoint = endpoint
c.msiType = msiTypeAzureArc
}
} else if endpoint, ok := os.LookupEnv(msiEndpoint); ok {
env = "Cloud Shell"
c.endpoint = endpoint
c.msiType = msiTypeCloudShell
} else {
setIMDSRetryOptionDefaults(&cp.Retry)
}
c.pipeline = runtime.NewPipeline(component, version, runtime.PipelineOptions{}, &cp)
if log.Should(EventAuthentication) {
log.Writef(EventAuthentication, "Managed Identity Credential will use %s managed identity", env)
}
return &c, nil
}
// provideToken acquires a token for MSAL's confidential.Client, which caches the token
func (c *managedIdentityClient) provideToken(ctx context.Context, params confidential.TokenProviderParameters) (confidential.TokenProviderResult, error) {
result := confidential.TokenProviderResult{}
tk, err := c.authenticate(ctx, c.id, params.Scopes)
if err == nil {
result.AccessToken = tk.Token
result.ExpiresInSeconds = int(time.Until(tk.ExpiresOn).Seconds())
}
return result, err
}
// authenticate acquires an access token
func (c *managedIdentityClient) authenticate(ctx context.Context, id ManagedIDKind, scopes []string) (azcore.AccessToken, error) {
msg, err := c.createAuthRequest(ctx, id, scopes)
if err != nil {
return azcore.AccessToken{}, err
}
resp, err := c.pipeline.Do(msg)
if err != nil {
return azcore.AccessToken{}, newAuthenticationFailedError(credNameManagedIdentity, err.Error(), nil, err)
}
if runtime.HasStatusCode(resp, http.StatusOK, http.StatusCreated) {
return c.createAccessToken(resp)
}
if c.msiType == msiTypeIMDS && resp.StatusCode == 400 {
if id != nil {
return azcore.AccessToken{}, newAuthenticationFailedError(credNameManagedIdentity, "the requested identity isn't assigned to this resource", resp, nil)
}
return azcore.AccessToken{}, newCredentialUnavailableError(credNameManagedIdentity, "no default identity is assigned to this resource")
}
return azcore.AccessToken{}, newAuthenticationFailedError(credNameManagedIdentity, "authentication failed", resp, nil)
}
func (c *managedIdentityClient) createAccessToken(res *http.Response) (azcore.AccessToken, error) {
value := struct {
// these are the only fields that we use
Token string `json:"access_token,omitempty"`
RefreshToken string `json:"refresh_token,omitempty"`
ExpiresIn wrappedNumber `json:"expires_in,omitempty"` // this field should always return the number of seconds for which a token is valid
ExpiresOn interface{} `json:"expires_on,omitempty"` // the value returned in this field varies between a number and a date string
}{}
if err := runtime.UnmarshalAsJSON(res, &value); err != nil {
return azcore.AccessToken{}, fmt.Errorf("internal AccessToken: %v", err)
}
if value.ExpiresIn != "" {
expiresIn, err := json.Number(value.ExpiresIn).Int64()
if err != nil {
return azcore.AccessToken{}, err
}
return azcore.AccessToken{Token: value.Token, ExpiresOn: time.Now().Add(time.Second * time.Duration(expiresIn)).UTC()}, nil
}
switch v := value.ExpiresOn.(type) {
case float64:
return azcore.AccessToken{Token: value.Token, ExpiresOn: time.Unix(int64(v), 0).UTC()}, nil
case string:
if expiresOn, err := strconv.Atoi(v); err == nil {
return azcore.AccessToken{Token: value.Token, ExpiresOn: time.Unix(int64(expiresOn), 0).UTC()}, nil
}
return azcore.AccessToken{}, newAuthenticationFailedError(credNameManagedIdentity, "unexpected expires_on value: "+v, res, nil)
default:
msg := fmt.Sprintf("unsupported type received in expires_on: %T, %v", v, v)
return azcore.AccessToken{}, newAuthenticationFailedError(credNameManagedIdentity, msg, res, nil)
}
}
func (c *managedIdentityClient) createAuthRequest(ctx context.Context, id ManagedIDKind, scopes []string) (*policy.Request, error) {
switch c.msiType {
case msiTypeIMDS:
return c.createIMDSAuthRequest(ctx, id, scopes)
case msiTypeAppService:
return c.createAppServiceAuthRequest(ctx, id, scopes)
case msiTypeAzureArc:
// need to perform preliminary request to retreive the secret key challenge provided by the HIMDS service
key, err := c.getAzureArcSecretKey(ctx, scopes)
if err != nil {
msg := fmt.Sprintf("failed to retreive secret key from the identity endpoint: %v", err)
return nil, newAuthenticationFailedError(credNameManagedIdentity, msg, nil, err)
}
return c.createAzureArcAuthRequest(ctx, id, scopes, key)
case msiTypeServiceFabric:
return c.createServiceFabricAuthRequest(ctx, id, scopes)
case msiTypeCloudShell:
return c.createCloudShellAuthRequest(ctx, id, scopes)
default:
return nil, newCredentialUnavailableError(credNameManagedIdentity, "managed identity isn't supported in this environment")
}
}
func (c *managedIdentityClient) createIMDSAuthRequest(ctx context.Context, id ManagedIDKind, scopes []string) (*policy.Request, error) {
request, err := runtime.NewRequest(ctx, http.MethodGet, c.endpoint)
if err != nil {
return nil, err
}
request.Raw().Header.Set(headerMetadata, "true")
q := request.Raw().URL.Query()
q.Add("api-version", imdsAPIVersion)
q.Add("resource", strings.Join(scopes, " "))
if id != nil {
if id.idKind() == miResourceID {
q.Add(qpResID, id.String())
} else {
q.Add(qpClientID, id.String())
}
}
request.Raw().URL.RawQuery = q.Encode()
return request, nil
}
func (c *managedIdentityClient) createAppServiceAuthRequest(ctx context.Context, id ManagedIDKind, scopes []string) (*policy.Request, error) {
request, err := runtime.NewRequest(ctx, http.MethodGet, c.endpoint)
if err != nil {
return nil, err
}
request.Raw().Header.Set("X-IDENTITY-HEADER", os.Getenv(identityHeader))
q := request.Raw().URL.Query()
q.Add("api-version", "2019-08-01")
q.Add("resource", scopes[0])
if id != nil {
if id.idKind() == miResourceID {
q.Add(qpResID, id.String())
} else {
q.Add(qpClientID, id.String())
}
}
request.Raw().URL.RawQuery = q.Encode()
return request, nil
}
func (c *managedIdentityClient) createServiceFabricAuthRequest(ctx context.Context, id ManagedIDKind, scopes []string) (*policy.Request, error) {
request, err := runtime.NewRequest(ctx, http.MethodGet, c.endpoint)
if err != nil {
return nil, err
}
q := request.Raw().URL.Query()
request.Raw().Header.Set("Accept", "application/json")
request.Raw().Header.Set("Secret", os.Getenv(identityHeader))
q.Add("api-version", serviceFabricAPIVersion)
q.Add("resource", strings.Join(scopes, " "))
if id != nil {
log.Write(EventAuthentication, "WARNING: Service Fabric doesn't support selecting a user-assigned identity at runtime")
if id.idKind() == miResourceID {
q.Add(qpResID, id.String())
} else {
q.Add(qpClientID, id.String())
}
}
request.Raw().URL.RawQuery = q.Encode()
return request, nil
}
func (c *managedIdentityClient) getAzureArcSecretKey(ctx context.Context, resources []string) (string, error) {
// create the request to retreive the secret key challenge provided by the HIMDS service
request, err := runtime.NewRequest(ctx, http.MethodGet, c.endpoint)
if err != nil {
return "", err
}
request.Raw().Header.Set(headerMetadata, "true")
q := request.Raw().URL.Query()
q.Add("api-version", azureArcAPIVersion)
q.Add("resource", strings.Join(resources, " "))
request.Raw().URL.RawQuery = q.Encode()
// send the initial request to get the short-lived secret key
response, err := c.pipeline.Do(request)
if err != nil {
return "", err
}
// the endpoint is expected to return a 401 with the WWW-Authenticate header set to the location
// of the secret key file. Any other status code indicates an error in the request.
if response.StatusCode != 401 {
msg := fmt.Sprintf("expected a 401 response, received %d", response.StatusCode)
return "", newAuthenticationFailedError(credNameManagedIdentity, msg, response, nil)
}
header := response.Header.Get("WWW-Authenticate")
if len(header) == 0 {
return "", errors.New("did not receive a value from WWW-Authenticate header")
}
// the WWW-Authenticate header is expected in the following format: Basic realm=/some/file/path.key
pos := strings.LastIndex(header, "=")
if pos == -1 {
return "", fmt.Errorf("did not receive a correct value from WWW-Authenticate header: %s", header)
}
key, err := os.ReadFile(header[pos+1:])
if err != nil {
return "", fmt.Errorf("could not read file (%s) contents: %v", header[pos+1:], err)
}
return string(key), nil
}
func (c *managedIdentityClient) createAzureArcAuthRequest(ctx context.Context, id ManagedIDKind, resources []string, key string) (*policy.Request, error) {
request, err := runtime.NewRequest(ctx, http.MethodGet, c.endpoint)
if err != nil {
return nil, err
}
request.Raw().Header.Set(headerMetadata, "true")
request.Raw().Header.Set("Authorization", fmt.Sprintf("Basic %s", key))
q := request.Raw().URL.Query()
q.Add("api-version", azureArcAPIVersion)
q.Add("resource", strings.Join(resources, " "))
if id != nil {
log.Write(EventAuthentication, "WARNING: Azure Arc doesn't support user-assigned managed identities")
if id.idKind() == miResourceID {
q.Add(qpResID, id.String())
} else {
q.Add(qpClientID, id.String())
}
}
request.Raw().URL.RawQuery = q.Encode()
return request, nil
}
func (c *managedIdentityClient) createCloudShellAuthRequest(ctx context.Context, id ManagedIDKind, scopes []string) (*policy.Request, error) {
request, err := runtime.NewRequest(ctx, http.MethodPost, c.endpoint)
if err != nil {
return nil, err
}
request.Raw().Header.Set(headerMetadata, "true")
data := url.Values{}
data.Set("resource", strings.Join(scopes, " "))
dataEncoded := data.Encode()
body := streaming.NopCloser(strings.NewReader(dataEncoded))
if err := request.SetBody(body, "application/x-www-form-urlencoded"); err != nil {
return nil, err
}
if id != nil {
log.Write(EventAuthentication, "WARNING: Cloud Shell doesn't support user-assigned managed identities")
q := request.Raw().URL.Query()
if id.idKind() == miResourceID {
q.Add(qpResID, id.String())
} else {
q.Add(qpClientID, id.String())
}
}
return request, nil
}

View file

@ -0,0 +1,127 @@
//go:build go1.18
// +build go1.18
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package azidentity
import (
"context"
"errors"
"fmt"
"strings"
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
"github.com/AzureAD/microsoft-authentication-library-for-go/apps/confidential"
)
const credNameManagedIdentity = "ManagedIdentityCredential"
type managedIdentityIDKind int
const (
miClientID managedIdentityIDKind = 0
miResourceID managedIdentityIDKind = 1
)
// ManagedIDKind identifies the ID of a managed identity as either a client or resource ID
type ManagedIDKind interface {
fmt.Stringer
idKind() managedIdentityIDKind
}
// ClientID is the client ID of a user-assigned managed identity.
type ClientID string
func (ClientID) idKind() managedIdentityIDKind {
return miClientID
}
// String returns the string value of the ID.
func (c ClientID) String() string {
return string(c)
}
// ResourceID is the resource ID of a user-assigned managed identity.
type ResourceID string
func (ResourceID) idKind() managedIdentityIDKind {
return miResourceID
}
// String returns the string value of the ID.
func (r ResourceID) String() string {
return string(r)
}
// ManagedIdentityCredentialOptions contains optional parameters for ManagedIdentityCredential.
type ManagedIdentityCredentialOptions struct {
azcore.ClientOptions
// ID is the ID of a managed identity the credential should authenticate. Set this field to use a specific identity
// instead of the hosting environment's default. The value may be the identity's client ID or resource ID, but note that
// some platforms don't accept resource IDs.
ID ManagedIDKind
}
// ManagedIdentityCredential authenticates an Azure managed identity in any hosting environment supporting managed identities.
// This credential authenticates a system-assigned identity by default. Use ManagedIdentityCredentialOptions.ID to specify a
// user-assigned identity. See Azure Active Directory documentation for more information about managed identities:
// https://docs.microsoft.com/azure/active-directory/managed-identities-azure-resources/overview
type ManagedIdentityCredential struct {
client confidentialClient
mic *managedIdentityClient
s *syncer
}
// NewManagedIdentityCredential creates a ManagedIdentityCredential. Pass nil to accept default options.
func NewManagedIdentityCredential(options *ManagedIdentityCredentialOptions) (*ManagedIdentityCredential, error) {
if options == nil {
options = &ManagedIdentityCredentialOptions{}
}
mic, err := newManagedIdentityClient(options)
if err != nil {
return nil, err
}
cred := confidential.NewCredFromTokenProvider(mic.provideToken)
// It's okay to give MSAL an invalid client ID because MSAL will use it only as part of a cache key.
// ManagedIdentityClient handles all the details of authentication and won't receive this value from MSAL.
clientID := "SYSTEM-ASSIGNED-MANAGED-IDENTITY"
if options.ID != nil {
clientID = options.ID.String()
}
// similarly, it's okay to give MSAL an incorrect authority URL because that URL won't be used
c, err := confidential.New("https://login.microsoftonline.com/common", clientID, cred)
if err != nil {
return nil, err
}
m := ManagedIdentityCredential{client: c, mic: mic}
m.s = newSyncer(credNameManagedIdentity, "", nil, m.requestToken, m.silentAuth)
return &m, nil
}
// GetToken requests an access token from the hosting environment. This method is called automatically by Azure SDK clients.
func (c *ManagedIdentityCredential) GetToken(ctx context.Context, opts policy.TokenRequestOptions) (azcore.AccessToken, error) {
if len(opts.Scopes) != 1 {
err := errors.New(credNameManagedIdentity + ": GetToken() requires exactly one scope")
return azcore.AccessToken{}, err
}
// managed identity endpoints require an AADv1 resource (i.e. token audience), not a v2 scope, so we remove "/.default" here
opts.Scopes = []string{strings.TrimSuffix(opts.Scopes[0], defaultSuffix)}
return c.s.GetToken(ctx, opts)
}
func (c *ManagedIdentityCredential) requestToken(ctx context.Context, opts policy.TokenRequestOptions) (azcore.AccessToken, error) {
ar, err := c.client.AcquireTokenByCredential(ctx, opts.Scopes)
return azcore.AccessToken{Token: ar.AccessToken, ExpiresOn: ar.ExpiresOn.UTC()}, err
}
func (c *ManagedIdentityCredential) silentAuth(ctx context.Context, opts policy.TokenRequestOptions) (azcore.AccessToken, error) {
ar, err := c.client.AcquireTokenSilent(ctx, opts.Scopes)
return azcore.AccessToken{Token: ar.AccessToken, ExpiresOn: ar.ExpiresOn.UTC()}, err
}
var _ azcore.TokenCredential = (*ManagedIdentityCredential)(nil)

View file

@ -0,0 +1,99 @@
//go:build go1.18
// +build go1.18
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package azidentity
import (
"context"
"crypto"
"crypto/x509"
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
"github.com/AzureAD/microsoft-authentication-library-for-go/apps/confidential"
)
const credNameOBO = "OnBehalfOfCredential"
// OnBehalfOfCredential authenticates a service principal via the on-behalf-of flow. This is typically used by
// middle-tier services that authorize requests to other services with a delegated user identity. Because this
// is not an interactive authentication flow, an application using it must have admin consent for any delegated
// permissions before requesting tokens for them. See [Azure Active Directory documentation] for more details.
//
// [Azure Active Directory documentation]: https://docs.microsoft.com/azure/active-directory/develop/v2-oauth2-on-behalf-of-flow
type OnBehalfOfCredential struct {
assertion string
client confidentialClient
s *syncer
}
// OnBehalfOfCredentialOptions contains optional parameters for OnBehalfOfCredential
type OnBehalfOfCredentialOptions struct {
azcore.ClientOptions
// AdditionallyAllowedTenants specifies additional tenants for which the credential may acquire tokens.
// Add the wildcard value "*" to allow the credential to acquire tokens for any tenant in which the
// application is registered.
AdditionallyAllowedTenants []string
// DisableInstanceDiscovery should be set true only by applications authenticating in disconnected clouds, or
// private clouds such as Azure Stack. It determines whether the credential requests Azure AD instance metadata
// from https://login.microsoft.com before authenticating. Setting this to true will skip this request, making
// the application responsible for ensuring the configured authority is valid and trustworthy.
DisableInstanceDiscovery bool
// SendCertificateChain applies only when the credential is configured to authenticate with a certificate.
// This setting controls whether the credential sends the public certificate chain in the x5c header of each
// token request's JWT. This is required for, and only used in, Subject Name/Issuer (SNI) authentication.
SendCertificateChain bool
}
// NewOnBehalfOfCredentialWithCertificate constructs an OnBehalfOfCredential that authenticates with a certificate.
// See [ParseCertificates] for help loading a certificate.
func NewOnBehalfOfCredentialWithCertificate(tenantID, clientID, userAssertion string, certs []*x509.Certificate, key crypto.PrivateKey, options *OnBehalfOfCredentialOptions) (*OnBehalfOfCredential, error) {
cred, err := confidential.NewCredFromCert(certs, key)
if err != nil {
return nil, err
}
return newOnBehalfOfCredential(tenantID, clientID, userAssertion, cred, options)
}
// NewOnBehalfOfCredentialWithSecret constructs an OnBehalfOfCredential that authenticates with a client secret.
func NewOnBehalfOfCredentialWithSecret(tenantID, clientID, userAssertion, clientSecret string, options *OnBehalfOfCredentialOptions) (*OnBehalfOfCredential, error) {
cred, err := confidential.NewCredFromSecret(clientSecret)
if err != nil {
return nil, err
}
return newOnBehalfOfCredential(tenantID, clientID, userAssertion, cred, options)
}
func newOnBehalfOfCredential(tenantID, clientID, userAssertion string, cred confidential.Credential, options *OnBehalfOfCredentialOptions) (*OnBehalfOfCredential, error) {
if options == nil {
options = &OnBehalfOfCredentialOptions{}
}
opts := []confidential.Option{}
if options.SendCertificateChain {
opts = append(opts, confidential.WithX5C())
}
opts = append(opts, confidential.WithInstanceDiscovery(!options.DisableInstanceDiscovery))
c, err := getConfidentialClient(clientID, tenantID, cred, &options.ClientOptions, opts...)
if err != nil {
return nil, err
}
obo := OnBehalfOfCredential{assertion: userAssertion, client: c}
obo.s = newSyncer(credNameOBO, tenantID, options.AdditionallyAllowedTenants, obo.requestToken, obo.requestToken)
return &obo, nil
}
// GetToken requests an access token from Azure Active Directory. This method is called automatically by Azure SDK clients.
func (o *OnBehalfOfCredential) GetToken(ctx context.Context, opts policy.TokenRequestOptions) (azcore.AccessToken, error) {
return o.s.GetToken(ctx, opts)
}
func (o *OnBehalfOfCredential) requestToken(ctx context.Context, opts policy.TokenRequestOptions) (azcore.AccessToken, error) {
ar, err := o.client.AcquireTokenOnBehalfOf(ctx, o.assertion, opts.Scopes, confidential.WithTenantID(opts.TenantID))
return azcore.AccessToken{Token: ar.AccessToken, ExpiresOn: ar.ExpiresOn.UTC()}, err
}
var _ azcore.TokenCredential = (*OnBehalfOfCredential)(nil)

View file

@ -0,0 +1,130 @@
//go:build go1.18
// +build go1.18
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package azidentity
import (
"context"
"errors"
"fmt"
"strings"
"sync"
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
"github.com/Azure/azure-sdk-for-go/sdk/internal/log"
)
type authFn func(context.Context, policy.TokenRequestOptions) (azcore.AccessToken, error)
// syncer synchronizes authentication calls so that goroutines can share a credential instance
type syncer struct {
addlTenants []string
authing bool
cond *sync.Cond
reqToken, silent authFn
name, tenant string
}
func newSyncer(name, tenant string, additionalTenants []string, reqToken, silentAuth authFn) *syncer {
return &syncer{
addlTenants: resolveAdditionalTenants(additionalTenants),
cond: &sync.Cond{L: &sync.Mutex{}},
name: name,
reqToken: reqToken,
silent: silentAuth,
tenant: tenant,
}
}
// GetToken ensures that only one goroutine authenticates at a time
func (s *syncer) GetToken(ctx context.Context, opts policy.TokenRequestOptions) (azcore.AccessToken, error) {
var at azcore.AccessToken
var err error
if len(opts.Scopes) == 0 {
return at, errors.New(s.name + ".GetToken() requires at least one scope")
}
// we don't resolve the tenant for managed identities because they can acquire tokens only from their home tenants
if s.name != credNameManagedIdentity {
tenant, err := s.resolveTenant(opts.TenantID)
if err != nil {
return at, err
}
opts.TenantID = tenant
}
auth := false
s.cond.L.Lock()
defer s.cond.L.Unlock()
for {
at, err = s.silent(ctx, opts)
if err == nil {
// got a token
break
}
if !s.authing {
// this goroutine will request a token
s.authing, auth = true, true
break
}
// another goroutine is acquiring a token; wait for it to finish, then try silent auth again
s.cond.Wait()
}
if auth {
s.authing = false
at, err = s.reqToken(ctx, opts)
s.cond.Broadcast()
}
if err != nil {
// Return credentialUnavailableError directly because that type affects the behavior of credential chains.
// Otherwise, return AuthenticationFailedError.
var unavailableErr *credentialUnavailableError
if !errors.As(err, &unavailableErr) {
res := getResponseFromError(err)
err = newAuthenticationFailedError(s.name, err.Error(), res, err)
}
} else if log.Should(EventAuthentication) {
scope := strings.Join(opts.Scopes, ", ")
msg := fmt.Sprintf(`%s.GetToken() acquired a token for scope "%s"\n`, s.name, scope)
log.Write(EventAuthentication, msg)
}
return at, err
}
// resolveTenant returns the correct tenant for a token request given the credential's
// configuration, or an error when the specified tenant isn't allowed by that configuration
func (s *syncer) resolveTenant(requested string) (string, error) {
if requested == "" || requested == s.tenant {
return s.tenant, nil
}
if s.tenant == "adfs" {
return "", errors.New("ADFS doesn't support tenants")
}
if !validTenantID(requested) {
return "", errors.New(tenantIDValidationErr)
}
for _, t := range s.addlTenants {
if t == "*" || t == requested {
return requested, nil
}
}
return "", fmt.Errorf(`%s isn't configured to acquire tokens for tenant %q. To enable acquiring tokens for this tenant add it to the AdditionallyAllowedTenants on the credential options, or add "*" to allow acquiring tokens for any tenant`, s.name, requested)
}
// resolveAdditionalTenants returns a copy of tenants, simplified when tenants contains a wildcard
func resolveAdditionalTenants(tenants []string) []string {
if len(tenants) == 0 {
return nil
}
for _, t := range tenants {
// a wildcard makes all other values redundant
if t == "*" {
return []string{"*"}
}
}
cp := make([]string, len(tenants))
copy(cp, tenants)
return cp
}

View file

@ -0,0 +1,81 @@
//go:build go1.18
// +build go1.18
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package azidentity
import (
"context"
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
"github.com/AzureAD/microsoft-authentication-library-for-go/apps/public"
)
const credNameUserPassword = "UsernamePasswordCredential"
// UsernamePasswordCredentialOptions contains optional parameters for UsernamePasswordCredential.
type UsernamePasswordCredentialOptions struct {
azcore.ClientOptions
// AdditionallyAllowedTenants specifies additional tenants for which the credential may acquire tokens.
// Add the wildcard value "*" to allow the credential to acquire tokens for any tenant in which the
// application is registered.
AdditionallyAllowedTenants []string
// DisableInstanceDiscovery should be set true only by applications authenticating in disconnected clouds, or
// private clouds such as Azure Stack. It determines whether the credential requests Azure AD instance metadata
// from https://login.microsoft.com before authenticating. Setting this to true will skip this request, making
// the application responsible for ensuring the configured authority is valid and trustworthy.
DisableInstanceDiscovery bool
}
// UsernamePasswordCredential authenticates a user with a password. Microsoft doesn't recommend this kind of authentication,
// because it's less secure than other authentication flows. This credential is not interactive, so it isn't compatible
// with any form of multi-factor authentication, and the application must already have user or admin consent.
// This credential can only authenticate work and school accounts; it can't authenticate Microsoft accounts.
type UsernamePasswordCredential struct {
account public.Account
client publicClient
password, username string
s *syncer
}
// NewUsernamePasswordCredential creates a UsernamePasswordCredential. clientID is the ID of the application the user
// will authenticate to. Pass nil for options to accept defaults.
func NewUsernamePasswordCredential(tenantID string, clientID string, username string, password string, options *UsernamePasswordCredentialOptions) (*UsernamePasswordCredential, error) {
if options == nil {
options = &UsernamePasswordCredentialOptions{}
}
c, err := getPublicClient(clientID, tenantID, &options.ClientOptions, public.WithInstanceDiscovery(!options.DisableInstanceDiscovery))
if err != nil {
return nil, err
}
upc := UsernamePasswordCredential{client: c, password: password, username: username}
upc.s = newSyncer(credNameUserPassword, tenantID, options.AdditionallyAllowedTenants, upc.requestToken, upc.silentAuth)
return &upc, nil
}
// GetToken requests an access token from Azure Active Directory. This method is called automatically by Azure SDK clients.
func (c *UsernamePasswordCredential) GetToken(ctx context.Context, opts policy.TokenRequestOptions) (azcore.AccessToken, error) {
return c.s.GetToken(ctx, opts)
}
func (c *UsernamePasswordCredential) requestToken(ctx context.Context, opts policy.TokenRequestOptions) (azcore.AccessToken, error) {
ar, err := c.client.AcquireTokenByUsernamePassword(ctx, opts.Scopes, c.username, c.password, public.WithTenantID(opts.TenantID))
if err == nil {
c.account = ar.Account
}
return azcore.AccessToken{Token: ar.AccessToken, ExpiresOn: ar.ExpiresOn.UTC()}, err
}
func (c *UsernamePasswordCredential) silentAuth(ctx context.Context, opts policy.TokenRequestOptions) (azcore.AccessToken, error) {
ar, err := c.client.AcquireTokenSilent(ctx, opts.Scopes,
public.WithSilentAccount(c.account),
public.WithTenantID(opts.TenantID),
)
return azcore.AccessToken{Token: ar.AccessToken, ExpiresOn: ar.ExpiresOn.UTC()}, err
}
var _ azcore.TokenCredential = (*UsernamePasswordCredential)(nil)

View file

@ -0,0 +1,15 @@
//go:build go1.18
// +build go1.18
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package azidentity
const (
// UserAgent is the string to be used in the user agent string when making requests.
component = "azidentity"
// Version is the semantic version (see http://semver.org) of this module.
version = "v1.3.0"
)

View file

@ -0,0 +1,126 @@
//go:build go1.18
// +build go1.18
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package azidentity
import (
"context"
"errors"
"os"
"sync"
"time"
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
)
const credNameWorkloadIdentity = "WorkloadIdentityCredential"
// WorkloadIdentityCredential supports Azure workload identity on Kubernetes.
// See [Azure Kubernetes Service documentation] for more information.
//
// [Azure Kubernetes Service documentation]: https://learn.microsoft.com/azure/aks/workload-identity-overview
type WorkloadIdentityCredential struct {
assertion, file string
cred *ClientAssertionCredential
expires time.Time
mtx *sync.RWMutex
}
// WorkloadIdentityCredentialOptions contains optional parameters for WorkloadIdentityCredential.
type WorkloadIdentityCredentialOptions struct {
azcore.ClientOptions
// AdditionallyAllowedTenants specifies additional tenants for which the credential may acquire tokens.
// Add the wildcard value "*" to allow the credential to acquire tokens for any tenant in which the
// application is registered.
AdditionallyAllowedTenants []string
// ClientID of the service principal. Defaults to the value of the environment variable AZURE_CLIENT_ID.
ClientID string
// DisableInstanceDiscovery should be set true only by applications authenticating in disconnected clouds, or
// private clouds such as Azure Stack. It determines whether the credential requests Azure AD instance metadata
// from https://login.microsoft.com before authenticating. Setting this to true will skip this request, making
// the application responsible for ensuring the configured authority is valid and trustworthy.
DisableInstanceDiscovery bool
// TenantID of the service principal. Defaults to the value of the environment variable AZURE_TENANT_ID.
TenantID string
// TokenFilePath is the path a file containing the workload identity token. Defaults to the value of the
// environment variable AZURE_FEDERATED_TOKEN_FILE.
TokenFilePath string
}
// NewWorkloadIdentityCredential constructs a WorkloadIdentityCredential. Service principal configuration is read
// from environment variables as set by the Azure workload identity webhook. Set options to override those values.
func NewWorkloadIdentityCredential(options *WorkloadIdentityCredentialOptions) (*WorkloadIdentityCredential, error) {
if options == nil {
options = &WorkloadIdentityCredentialOptions{}
}
ok := false
clientID := options.ClientID
if clientID == "" {
if clientID, ok = os.LookupEnv(azureClientID); !ok {
return nil, errors.New("no client ID specified. Check pod configuration or set ClientID in the options")
}
}
file := options.TokenFilePath
if file == "" {
if file, ok = os.LookupEnv(azureFederatedTokenFile); !ok {
return nil, errors.New("no token file specified. Check pod configuration or set TokenFilePath in the options")
}
}
tenantID := options.TenantID
if tenantID == "" {
if tenantID, ok = os.LookupEnv(azureTenantID); !ok {
return nil, errors.New("no tenant ID specified. Check pod configuration or set TenantID in the options")
}
}
w := WorkloadIdentityCredential{file: file, mtx: &sync.RWMutex{}}
caco := ClientAssertionCredentialOptions{
AdditionallyAllowedTenants: options.AdditionallyAllowedTenants,
ClientOptions: options.ClientOptions,
DisableInstanceDiscovery: options.DisableInstanceDiscovery,
}
cred, err := NewClientAssertionCredential(tenantID, clientID, w.getAssertion, &caco)
if err != nil {
return nil, err
}
// we want "WorkloadIdentityCredential" in log messages, not "ClientAssertionCredential"
cred.s.name = credNameWorkloadIdentity
w.cred = cred
return &w, nil
}
// GetToken requests an access token from Azure Active Directory. Azure SDK clients call this method automatically.
func (w *WorkloadIdentityCredential) GetToken(ctx context.Context, opts policy.TokenRequestOptions) (azcore.AccessToken, error) {
return w.cred.GetToken(ctx, opts)
}
// getAssertion returns the specified file's content, which is expected to be a Kubernetes service account token.
// Kubernetes is responsible for updating the file as service account tokens expire.
func (w *WorkloadIdentityCredential) getAssertion(context.Context) (string, error) {
w.mtx.RLock()
if w.expires.Before(time.Now()) {
// ensure only one goroutine at a time updates the assertion
w.mtx.RUnlock()
w.mtx.Lock()
defer w.mtx.Unlock()
// double check because another goroutine may have acquired the write lock first and done the update
if now := time.Now(); w.expires.Before(now) {
content, err := os.ReadFile(w.file)
if err != nil {
return "", err
}
w.assertion = string(content)
// Kubernetes rotates service account tokens when they reach 80% of their total TTL. The shortest TTL
// is 1 hour. That implies the token we just read is valid for at least 12 minutes (20% of 1 hour),
// but we add some margin for safety.
w.expires = now.Add(10 * time.Minute)
}
} else {
defer w.mtx.RUnlock()
}
return w.assertion, nil
}

View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) Microsoft Corporation.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE

View file

@ -0,0 +1,54 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
/*
Package cache allows third parties to implement external storage for caching token data
for distributed systems or multiple local applications access.
The data stored and extracted will represent the entire cache. Therefore it is recommended
one msal instance per user. This data is considered opaque and there are no guarantees to
implementers on the format being passed.
*/
package cache
import "context"
// Marshaler marshals data from an internal cache to bytes that can be stored.
type Marshaler interface {
Marshal() ([]byte, error)
}
// Unmarshaler unmarshals data from a storage medium into the internal cache, overwriting it.
type Unmarshaler interface {
Unmarshal([]byte) error
}
// Serializer can serialize the cache to binary or from binary into the cache.
type Serializer interface {
Marshaler
Unmarshaler
}
// ExportHints are suggestions for storing data.
type ExportHints struct {
// PartitionKey is a suggested key for partitioning the cache
PartitionKey string
}
// ReplaceHints are suggestions for loading data.
type ReplaceHints struct {
// PartitionKey is a suggested key for partitioning the cache
PartitionKey string
}
// ExportReplace exports and replaces in-memory cache data. It doesn't support nil Context or
// define the outcome of passing one. A Context without a timeout must receive a default timeout
// specified by the implementor. Retries must be implemented inside the implementation.
type ExportReplace interface {
// Replace replaces the cache with what is in external storage. Implementors should honor
// Context cancellations and return context.Canceled or context.DeadlineExceeded in those cases.
Replace(ctx context.Context, cache Unmarshaler, hints ReplaceHints) error
// Export writes the binary representation of the cache (cache.Marshal()) to external storage.
// This is considered opaque. Context cancellations should be honored as in Replace.
Export(ctx context.Context, cache Marshaler, hints ExportHints) error
}

View file

@ -0,0 +1,685 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
/*
Package confidential provides a client for authentication of "confidential" applications.
A "confidential" application is defined as an app that run on servers. They are considered
difficult to access and for that reason capable of keeping an application secret.
Confidential clients can hold configuration-time secrets.
*/
package confidential
import (
"context"
"crypto"
"crypto/rsa"
"crypto/x509"
"encoding/base64"
"encoding/pem"
"errors"
"fmt"
"github.com/AzureAD/microsoft-authentication-library-for-go/apps/cache"
"github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/base"
"github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/exported"
"github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/oauth"
"github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/oauth/ops"
"github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/oauth/ops/accesstokens"
"github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/oauth/ops/authority"
"github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/options"
"github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/shared"
)
/*
Design note:
confidential.Client uses base.Client as an embedded type. base.Client statically assigns its attributes
during creation. As it doesn't have any pointers in it, anything borrowed from it, such as
Base.AuthParams is a copy that is free to be manipulated here.
Duplicate Calls shared between public.Client and this package:
There is some duplicate call options provided here that are the same as in public.Client . This
is a design choices. Go proverb(https://www.youtube.com/watch?v=PAAkCSZUG1c&t=9m28s):
"a little copying is better than a little dependency". Yes, we could have another package with
shared options (fail). That divides like 2 options from all others which makes the user look
through more docs. We can have all clients in one package, but I think separate packages
here makes for better naming (public.Client vs client.PublicClient). So I chose a little
duplication.
.Net People, Take note on X509:
This uses x509.Certificates and private keys. x509 does not store private keys. .Net
has some x509.Certificate2 thing that has private keys, but that is just some bullcrap that .Net
added, it doesn't exist in real life. As such I've put a PEM decoder into here.
*/
// TODO(msal): This should have example code for each method on client using Go's example doc framework.
// base usage details should be include in the package documentation.
// AuthResult contains the results of one token acquisition operation.
// For details see https://aka.ms/msal-net-authenticationresult
type AuthResult = base.AuthResult
type Account = shared.Account
// CertFromPEM converts a PEM file (.pem or .key) for use with [NewCredFromCert]. The file
// must contain the public certificate and the private key. If a PEM block is encrypted and
// password is not an empty string, it attempts to decrypt the PEM blocks using the password.
// Multiple certs are due to certificate chaining for use cases like TLS that sign from root to leaf.
func CertFromPEM(pemData []byte, password string) ([]*x509.Certificate, crypto.PrivateKey, error) {
var certs []*x509.Certificate
var priv crypto.PrivateKey
for {
block, rest := pem.Decode(pemData)
if block == nil {
break
}
//nolint:staticcheck // x509.IsEncryptedPEMBlock and x509.DecryptPEMBlock are deprecated. They are used here only to support a usecase.
if x509.IsEncryptedPEMBlock(block) {
b, err := x509.DecryptPEMBlock(block, []byte(password))
if err != nil {
return nil, nil, fmt.Errorf("could not decrypt encrypted PEM block: %v", err)
}
block, _ = pem.Decode(b)
if block == nil {
return nil, nil, fmt.Errorf("encounter encrypted PEM block that did not decode")
}
}
switch block.Type {
case "CERTIFICATE":
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return nil, nil, fmt.Errorf("block labelled 'CERTIFICATE' could not be parsed by x509: %v", err)
}
certs = append(certs, cert)
case "PRIVATE KEY":
if priv != nil {
return nil, nil, errors.New("found multiple private key blocks")
}
var err error
priv, err = x509.ParsePKCS8PrivateKey(block.Bytes)
if err != nil {
return nil, nil, fmt.Errorf("could not decode private key: %v", err)
}
case "RSA PRIVATE KEY":
if priv != nil {
return nil, nil, errors.New("found multiple private key blocks")
}
var err error
priv, err = x509.ParsePKCS1PrivateKey(block.Bytes)
if err != nil {
return nil, nil, fmt.Errorf("could not decode private key: %v", err)
}
}
pemData = rest
}
if len(certs) == 0 {
return nil, nil, fmt.Errorf("no certificates found")
}
if priv == nil {
return nil, nil, fmt.Errorf("no private key found")
}
return certs, priv, nil
}
// AssertionRequestOptions has required information for client assertion claims
type AssertionRequestOptions = exported.AssertionRequestOptions
// Credential represents the credential used in confidential client flows.
type Credential struct {
secret string
cert *x509.Certificate
key crypto.PrivateKey
x5c []string
assertionCallback func(context.Context, AssertionRequestOptions) (string, error)
tokenProvider func(context.Context, TokenProviderParameters) (TokenProviderResult, error)
}
// toInternal returns the accesstokens.Credential that is used internally. The current structure of the
// code requires that client.go, requests.go and confidential.go share a credential type without
// having import recursion. That requires the type used between is in a shared package. Therefore
// we have this.
func (c Credential) toInternal() (*accesstokens.Credential, error) {
if c.secret != "" {
return &accesstokens.Credential{Secret: c.secret}, nil
}
if c.cert != nil {
if c.key == nil {
return nil, errors.New("missing private key for certificate")
}
return &accesstokens.Credential{Cert: c.cert, Key: c.key, X5c: c.x5c}, nil
}
if c.key != nil {
return nil, errors.New("missing certificate for private key")
}
if c.assertionCallback != nil {
return &accesstokens.Credential{AssertionCallback: c.assertionCallback}, nil
}
if c.tokenProvider != nil {
return &accesstokens.Credential{TokenProvider: c.tokenProvider}, nil
}
return nil, errors.New("invalid credential")
}
// NewCredFromSecret creates a Credential from a secret.
func NewCredFromSecret(secret string) (Credential, error) {
if secret == "" {
return Credential{}, errors.New("secret can't be empty string")
}
return Credential{secret: secret}, nil
}
// NewCredFromAssertionCallback creates a Credential that invokes a callback to get assertions
// authenticating the application. The callback must be thread safe.
func NewCredFromAssertionCallback(callback func(context.Context, AssertionRequestOptions) (string, error)) Credential {
return Credential{assertionCallback: callback}
}
// NewCredFromCert creates a Credential from a certificate or chain of certificates and an RSA private key
// as returned by [CertFromPEM].
func NewCredFromCert(certs []*x509.Certificate, key crypto.PrivateKey) (Credential, error) {
cred := Credential{key: key}
k, ok := key.(*rsa.PrivateKey)
if !ok {
return cred, errors.New("key must be an RSA key")
}
for _, cert := range certs {
if cert == nil {
// not returning an error here because certs may still contain a sufficient cert/key pair
continue
}
certKey, ok := cert.PublicKey.(*rsa.PublicKey)
if ok && k.E == certKey.E && k.N.Cmp(certKey.N) == 0 {
// We know this is the signing cert because its public key matches the given private key.
// This cert must be first in x5c.
cred.cert = cert
cred.x5c = append([]string{base64.StdEncoding.EncodeToString(cert.Raw)}, cred.x5c...)
} else {
cred.x5c = append(cred.x5c, base64.StdEncoding.EncodeToString(cert.Raw))
}
}
if cred.cert == nil {
return cred, errors.New("key doesn't match any certificate")
}
return cred, nil
}
// TokenProviderParameters is the authentication parameters passed to token providers
type TokenProviderParameters = exported.TokenProviderParameters
// TokenProviderResult is the authentication result returned by custom token providers
type TokenProviderResult = exported.TokenProviderResult
// NewCredFromTokenProvider creates a Credential from a function that provides access tokens. The function
// must be concurrency safe. This is intended only to allow the Azure SDK to cache MSI tokens. It isn't
// useful to applications in general because the token provider must implement all authentication logic.
func NewCredFromTokenProvider(provider func(context.Context, TokenProviderParameters) (TokenProviderResult, error)) Credential {
return Credential{tokenProvider: provider}
}
// AutoDetectRegion instructs MSAL Go to auto detect region for Azure regional token service.
func AutoDetectRegion() string {
return "TryAutoDetect"
}
// Client is a representation of authentication client for confidential applications as defined in the
// package doc. A new Client should be created PER SERVICE USER.
// For more information, visit https://docs.microsoft.com/azure/active-directory/develop/msal-client-applications
type Client struct {
base base.Client
cred *accesstokens.Credential
}
// clientOptions are optional settings for New(). These options are set using various functions
// returning Option calls.
type clientOptions struct {
accessor cache.ExportReplace
authority, azureRegion string
capabilities []string
disableInstanceDiscovery, sendX5C bool
httpClient ops.HTTPClient
}
// Option is an optional argument to New().
type Option func(o *clientOptions)
// WithCache provides an accessor that will read and write authentication data to an externally managed cache.
func WithCache(accessor cache.ExportReplace) Option {
return func(o *clientOptions) {
o.accessor = accessor
}
}
// WithClientCapabilities allows configuring one or more client capabilities such as "CP1"
func WithClientCapabilities(capabilities []string) Option {
return func(o *clientOptions) {
// there's no danger of sharing the slice's underlying memory with the application because
// this slice is simply passed to base.WithClientCapabilities, which copies its data
o.capabilities = capabilities
}
}
// WithHTTPClient allows for a custom HTTP client to be set.
func WithHTTPClient(httpClient ops.HTTPClient) Option {
return func(o *clientOptions) {
o.httpClient = httpClient
}
}
// WithX5C specifies if x5c claim(public key of the certificate) should be sent to STS to enable Subject Name Issuer Authentication.
func WithX5C() Option {
return func(o *clientOptions) {
o.sendX5C = true
}
}
// WithInstanceDiscovery set to false to disable authority validation (to support private cloud scenarios)
func WithInstanceDiscovery(enabled bool) Option {
return func(o *clientOptions) {
o.disableInstanceDiscovery = !enabled
}
}
// WithAzureRegion sets the region(preferred) or Confidential.AutoDetectRegion() for auto detecting region.
// Region names as per https://azure.microsoft.com/en-ca/global-infrastructure/geographies/.
// See https://aka.ms/region-map for more details on region names.
// The region value should be short region name for the region where the service is deployed.
// For example "centralus" is short name for region Central US.
// Not all auth flows can use the regional token service.
// Service To Service (client credential flow) tokens can be obtained from the regional service.
// Requires configuration at the tenant level.
// Auto-detection works on a limited number of Azure artifacts (VMs, Azure functions).
// If auto-detection fails, the non-regional endpoint will be used.
// If an invalid region name is provided, the non-regional endpoint MIGHT be used or the token request MIGHT fail.
func WithAzureRegion(val string) Option {
return func(o *clientOptions) {
o.azureRegion = val
}
}
// New is the constructor for Client. authority is the URL of a token authority such as "https://login.microsoftonline.com/<your tenant>".
// If the Client will connect directly to AD FS, use "adfs" for the tenant. clientID is the application's client ID (also called its
// "application ID").
func New(authority, clientID string, cred Credential, options ...Option) (Client, error) {
internalCred, err := cred.toInternal()
if err != nil {
return Client{}, err
}
opts := clientOptions{
authority: authority,
// if the caller specified a token provider, it will handle all details of authentication, using Client only as a token cache
disableInstanceDiscovery: cred.tokenProvider != nil,
httpClient: shared.DefaultClient,
}
for _, o := range options {
o(&opts)
}
baseOpts := []base.Option{
base.WithCacheAccessor(opts.accessor),
base.WithClientCapabilities(opts.capabilities),
base.WithInstanceDiscovery(!opts.disableInstanceDiscovery),
base.WithRegionDetection(opts.azureRegion),
base.WithX5C(opts.sendX5C),
}
base, err := base.New(clientID, opts.authority, oauth.New(opts.httpClient), baseOpts...)
if err != nil {
return Client{}, err
}
base.AuthParams.IsConfidentialClient = true
return Client{base: base, cred: internalCred}, nil
}
// authCodeURLOptions contains options for AuthCodeURL
type authCodeURLOptions struct {
claims, loginHint, tenantID, domainHint string
}
// AuthCodeURLOption is implemented by options for AuthCodeURL
type AuthCodeURLOption interface {
authCodeURLOption()
}
// AuthCodeURL creates a URL used to acquire an authorization code. Users need to call CreateAuthorizationCodeURLParameters and pass it in.
//
// Options: [WithClaims], [WithDomainHint], [WithLoginHint], [WithTenantID]
func (cca Client) AuthCodeURL(ctx context.Context, clientID, redirectURI string, scopes []string, opts ...AuthCodeURLOption) (string, error) {
o := authCodeURLOptions{}
if err := options.ApplyOptions(&o, opts); err != nil {
return "", err
}
ap, err := cca.base.AuthParams.WithTenant(o.tenantID)
if err != nil {
return "", err
}
ap.Claims = o.claims
ap.LoginHint = o.loginHint
ap.DomainHint = o.domainHint
return cca.base.AuthCodeURL(ctx, clientID, redirectURI, scopes, ap)
}
// WithLoginHint pre-populates the login prompt with a username.
func WithLoginHint(username string) interface {
AuthCodeURLOption
options.CallOption
} {
return struct {
AuthCodeURLOption
options.CallOption
}{
CallOption: options.NewCallOption(
func(a any) error {
switch t := a.(type) {
case *authCodeURLOptions:
t.loginHint = username
default:
return fmt.Errorf("unexpected options type %T", a)
}
return nil
},
),
}
}
// WithDomainHint adds the IdP domain as domain_hint query parameter in the auth url.
func WithDomainHint(domain string) interface {
AuthCodeURLOption
options.CallOption
} {
return struct {
AuthCodeURLOption
options.CallOption
}{
CallOption: options.NewCallOption(
func(a any) error {
switch t := a.(type) {
case *authCodeURLOptions:
t.domainHint = domain
default:
return fmt.Errorf("unexpected options type %T", a)
}
return nil
},
),
}
}
// WithClaims sets additional claims to request for the token, such as those required by conditional access policies.
// Use this option when Azure AD returned a claims challenge for a prior request. The argument must be decoded.
// This option is valid for any token acquisition method.
func WithClaims(claims string) interface {
AcquireByAuthCodeOption
AcquireByCredentialOption
AcquireOnBehalfOfOption
AcquireSilentOption
AuthCodeURLOption
options.CallOption
} {
return struct {
AcquireByAuthCodeOption
AcquireByCredentialOption
AcquireOnBehalfOfOption
AcquireSilentOption
AuthCodeURLOption
options.CallOption
}{
CallOption: options.NewCallOption(
func(a any) error {
switch t := a.(type) {
case *acquireTokenByAuthCodeOptions:
t.claims = claims
case *acquireTokenByCredentialOptions:
t.claims = claims
case *acquireTokenOnBehalfOfOptions:
t.claims = claims
case *acquireTokenSilentOptions:
t.claims = claims
case *authCodeURLOptions:
t.claims = claims
default:
return fmt.Errorf("unexpected options type %T", a)
}
return nil
},
),
}
}
// WithTenantID specifies a tenant for a single authentication. It may be different than the tenant set in [New].
// This option is valid for any token acquisition method.
func WithTenantID(tenantID string) interface {
AcquireByAuthCodeOption
AcquireByCredentialOption
AcquireOnBehalfOfOption
AcquireSilentOption
AuthCodeURLOption
options.CallOption
} {
return struct {
AcquireByAuthCodeOption
AcquireByCredentialOption
AcquireOnBehalfOfOption
AcquireSilentOption
AuthCodeURLOption
options.CallOption
}{
CallOption: options.NewCallOption(
func(a any) error {
switch t := a.(type) {
case *acquireTokenByAuthCodeOptions:
t.tenantID = tenantID
case *acquireTokenByCredentialOptions:
t.tenantID = tenantID
case *acquireTokenOnBehalfOfOptions:
t.tenantID = tenantID
case *acquireTokenSilentOptions:
t.tenantID = tenantID
case *authCodeURLOptions:
t.tenantID = tenantID
default:
return fmt.Errorf("unexpected options type %T", a)
}
return nil
},
),
}
}
// acquireTokenSilentOptions are all the optional settings to an AcquireTokenSilent() call.
// These are set by using various AcquireTokenSilentOption functions.
type acquireTokenSilentOptions struct {
account Account
claims, tenantID string
}
// AcquireSilentOption is implemented by options for AcquireTokenSilent
type AcquireSilentOption interface {
acquireSilentOption()
}
// WithSilentAccount uses the passed account during an AcquireTokenSilent() call.
func WithSilentAccount(account Account) interface {
AcquireSilentOption
options.CallOption
} {
return struct {
AcquireSilentOption
options.CallOption
}{
CallOption: options.NewCallOption(
func(a any) error {
switch t := a.(type) {
case *acquireTokenSilentOptions:
t.account = account
default:
return fmt.Errorf("unexpected options type %T", a)
}
return nil
},
),
}
}
// AcquireTokenSilent acquires a token from either the cache or using a refresh token.
//
// Options: [WithClaims], [WithSilentAccount], [WithTenantID]
func (cca Client) AcquireTokenSilent(ctx context.Context, scopes []string, opts ...AcquireSilentOption) (AuthResult, error) {
o := acquireTokenSilentOptions{}
if err := options.ApplyOptions(&o, opts); err != nil {
return AuthResult{}, err
}
if o.claims != "" {
return AuthResult{}, errors.New("call another AcquireToken method to request a new token having these claims")
}
silentParameters := base.AcquireTokenSilentParameters{
Scopes: scopes,
Account: o.account,
RequestType: accesstokens.ATConfidential,
Credential: cca.cred,
IsAppCache: o.account.IsZero(),
TenantID: o.tenantID,
}
return cca.base.AcquireTokenSilent(ctx, silentParameters)
}
// acquireTokenByAuthCodeOptions contains the optional parameters used to acquire an access token using the authorization code flow.
type acquireTokenByAuthCodeOptions struct {
challenge, claims, tenantID string
}
// AcquireByAuthCodeOption is implemented by options for AcquireTokenByAuthCode
type AcquireByAuthCodeOption interface {
acquireByAuthCodeOption()
}
// WithChallenge allows you to provide a challenge for the .AcquireTokenByAuthCode() call.
func WithChallenge(challenge string) interface {
AcquireByAuthCodeOption
options.CallOption
} {
return struct {
AcquireByAuthCodeOption
options.CallOption
}{
CallOption: options.NewCallOption(
func(a any) error {
switch t := a.(type) {
case *acquireTokenByAuthCodeOptions:
t.challenge = challenge
default:
return fmt.Errorf("unexpected options type %T", a)
}
return nil
},
),
}
}
// AcquireTokenByAuthCode is a request to acquire a security token from the authority, using an authorization code.
// The specified redirect URI must be the same URI that was used when the authorization code was requested.
//
// Options: [WithChallenge], [WithClaims], [WithTenantID]
func (cca Client) AcquireTokenByAuthCode(ctx context.Context, code string, redirectURI string, scopes []string, opts ...AcquireByAuthCodeOption) (AuthResult, error) {
o := acquireTokenByAuthCodeOptions{}
if err := options.ApplyOptions(&o, opts); err != nil {
return AuthResult{}, err
}
params := base.AcquireTokenAuthCodeParameters{
Scopes: scopes,
Code: code,
Challenge: o.challenge,
Claims: o.claims,
AppType: accesstokens.ATConfidential,
Credential: cca.cred, // This setting differs from public.Client.AcquireTokenByAuthCode
RedirectURI: redirectURI,
TenantID: o.tenantID,
}
return cca.base.AcquireTokenByAuthCode(ctx, params)
}
// acquireTokenByCredentialOptions contains optional configuration for AcquireTokenByCredential
type acquireTokenByCredentialOptions struct {
claims, tenantID string
}
// AcquireByCredentialOption is implemented by options for AcquireTokenByCredential
type AcquireByCredentialOption interface {
acquireByCredOption()
}
// AcquireTokenByCredential acquires a security token from the authority, using the client credentials grant.
//
// Options: [WithClaims], [WithTenantID]
func (cca Client) AcquireTokenByCredential(ctx context.Context, scopes []string, opts ...AcquireByCredentialOption) (AuthResult, error) {
o := acquireTokenByCredentialOptions{}
err := options.ApplyOptions(&o, opts)
if err != nil {
return AuthResult{}, err
}
authParams, err := cca.base.AuthParams.WithTenant(o.tenantID)
if err != nil {
return AuthResult{}, err
}
authParams.Scopes = scopes
authParams.AuthorizationType = authority.ATClientCredentials
authParams.Claims = o.claims
token, err := cca.base.Token.Credential(ctx, authParams, cca.cred)
if err != nil {
return AuthResult{}, err
}
return cca.base.AuthResultFromToken(ctx, authParams, token, true)
}
// acquireTokenOnBehalfOfOptions contains optional configuration for AcquireTokenOnBehalfOf
type acquireTokenOnBehalfOfOptions struct {
claims, tenantID string
}
// AcquireOnBehalfOfOption is implemented by options for AcquireTokenOnBehalfOf
type AcquireOnBehalfOfOption interface {
acquireOBOOption()
}
// AcquireTokenOnBehalfOf acquires a security token for an app using middle tier apps access token.
// Refer https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-on-behalf-of-flow.
//
// Options: [WithClaims], [WithTenantID]
func (cca Client) AcquireTokenOnBehalfOf(ctx context.Context, userAssertion string, scopes []string, opts ...AcquireOnBehalfOfOption) (AuthResult, error) {
o := acquireTokenOnBehalfOfOptions{}
if err := options.ApplyOptions(&o, opts); err != nil {
return AuthResult{}, err
}
params := base.AcquireTokenOnBehalfOfParameters{
Scopes: scopes,
UserAssertion: userAssertion,
Claims: o.claims,
Credential: cca.cred,
TenantID: o.tenantID,
}
return cca.base.AcquireTokenOnBehalfOf(ctx, params)
}
// Account gets the account in the token cache with the specified homeAccountID.
func (cca Client) Account(ctx context.Context, accountID string) (Account, error) {
return cca.base.Account(ctx, accountID)
}
// RemoveAccount signs the account out and forgets account from token cache.
func (cca Client) RemoveAccount(ctx context.Context, account Account) error {
return cca.base.RemoveAccount(ctx, account)
}

View file

@ -0,0 +1,111 @@
# MSAL Error Design
Author: Abhidnya Patil(abhidnya.patil@microsoft.com)
Contributors:
- John Doak(jdoak@microsoft.com)
- Keegan Caruso(Keegan.Caruso@microsoft.com)
- Joel Hendrix(jhendrix@microsoft.com)
## Background
Errors in MSAL are intended for app developers to troubleshoot and not for displaying to end-users.
### Go error handling vs other MSAL languages
Most modern languages use exception based errors. Simply put, you "throw" an exception and it must be caught at some routine in the upper stack or it will eventually crash the program.
Go doesn't use exceptions, instead it relies on multiple return values, one of which can be the builtin error interface type. It is up to the user to decide what to do.
### Go custom error types
Errors can be created in Go by simply using errors.New() or fmt.Errorf() to create an "error".
Custom errors can be created in multiple ways. One of the more robust ways is simply to satisfy the error interface:
```go
type MyCustomErr struct {
Msg string
}
func (m MyCustomErr) Error() string { // This implements "error"
return m.Msg
}
```
### MSAL Error Goals
- Provide diagnostics to the user and for tickets that can be used to track down bugs or client misconfigurations
- Detect errors that are transitory and can be retried
- Allow the user to identify certain errors that the program can respond to, such a informing the user for the need to do an enrollment
## Implementing Client Side Errors
Client side errors indicate a misconfiguration or passing of bad arguments that is non-recoverable. Retrying isn't possible.
These errors can simply be standard Go errors created by errors.New() or fmt.Errorf(). If down the line we need a custom error, we can introduce it, but for now the error messages just need to be clear on what the issue was.
## Implementing Service Side Errors
Service side errors occur when an external RPC responds either with an HTTP error code or returns a message that includes an error.
These errors can be transitory (please slow down) or permanent (HTTP 404). To provide our diagnostic goals, we require the ability to differentiate these errors from other errors.
The current implementation includes a specialized type that captures any error from the server:
```go
// CallErr represents an HTTP call error. Has a Verbose() method that allows getting the
// http.Request and Response objects. Implements error.
type CallErr struct {
Req *http.Request
Resp *http.Response
Err error
}
// Errors implements error.Error().
func (e CallErr) Error() string {
return e.Err.Error()
}
// Verbose prints a versbose error message with the request or response.
func (e CallErr) Verbose() string {
e.Resp.Request = nil // This brings in a bunch of TLS stuff we don't need
e.Resp.TLS = nil // Same
return fmt.Sprintf("%s:\nRequest:\n%s\nResponse:\n%s", e.Err, prettyConf.Sprint(e.Req), prettyConf.Sprint(e.Resp))
}
```
A user will always receive the most concise error we provide. They can tell if it is a server side error using Go error package:
```go
var callErr CallErr
if errors.As(err, &callErr) {
...
}
```
We provide a Verbose() function that can retrieve the most verbose message from any error we provide:
```go
fmt.Println(errors.Verbose(err))
```
If further differentiation is required, we can add custom errors that use Go error wrapping on top of CallErr to achieve our diagnostic goals (such as detecting when to retry a call due to transient errors).
CallErr is always thrown from the comm package (which handles all http requests) and looks similar to:
```go
return nil, errors.CallErr{
Req: req,
Resp: reply,
Err: fmt.Errorf("http call(%s)(%s) error: reply status code was %d:\n%s", req.URL.String(), req.Method, reply.StatusCode, ErrorResponse), //ErrorResponse is the json body extracted from the http response
}
```
## Future Decisions
The ability to retry calls needs to have centralized responsibility. Either the user is doing it or the client is doing it.
If the user should be responsible, our errors package will include a CanRetry() function that will inform the user if the error provided to them is retryable. This is based on the http error code and possibly the type of error that was returned. It would also include a sleep time if the server returned an amount of time to wait.
Otherwise we will do this internally and retries will be left to us.

View file

@ -0,0 +1,89 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
package errors
import (
"errors"
"fmt"
"io"
"net/http"
"reflect"
"strings"
"github.com/kylelemons/godebug/pretty"
)
var prettyConf = &pretty.Config{
IncludeUnexported: false,
SkipZeroFields: true,
TrackCycles: true,
Formatter: map[reflect.Type]interface{}{
reflect.TypeOf((*io.Reader)(nil)).Elem(): func(r io.Reader) string {
b, err := io.ReadAll(r)
if err != nil {
return "could not read io.Reader content"
}
return string(b)
},
},
}
type verboser interface {
Verbose() string
}
// Verbose prints the most verbose error that the error message has.
func Verbose(err error) string {
build := strings.Builder{}
for {
if err == nil {
break
}
if v, ok := err.(verboser); ok {
build.WriteString(v.Verbose())
} else {
build.WriteString(err.Error())
}
err = errors.Unwrap(err)
}
return build.String()
}
// New is equivalent to errors.New().
func New(text string) error {
return errors.New(text)
}
// CallErr represents an HTTP call error. Has a Verbose() method that allows getting the
// http.Request and Response objects. Implements error.
type CallErr struct {
Req *http.Request
// Resp contains response body
Resp *http.Response
Err error
}
// Errors implements error.Error().
func (e CallErr) Error() string {
return e.Err.Error()
}
// Verbose prints a versbose error message with the request or response.
func (e CallErr) Verbose() string {
e.Resp.Request = nil // This brings in a bunch of TLS crap we don't need
e.Resp.TLS = nil // Same
return fmt.Sprintf("%s:\nRequest:\n%s\nResponse:\n%s", e.Err, prettyConf.Sprint(e.Req), prettyConf.Sprint(e.Resp))
}
// Is reports whether any error in errors chain matches target.
func Is(err, target error) bool {
return errors.Is(err, target)
}
// As finds the first error in errors chain that matches target,
// and if so, sets target to that error value and returns true.
// Otherwise, it returns false.
func As(err error, target interface{}) bool {
return errors.As(err, target)
}

View file

@ -0,0 +1,467 @@
// Package base contains a "Base" client that is used by the external public.Client and confidential.Client.
// Base holds shared attributes that must be available to both clients and methods that act as
// shared calls.
package base
import (
"context"
"errors"
"fmt"
"net/url"
"reflect"
"strings"
"sync"
"time"
"github.com/AzureAD/microsoft-authentication-library-for-go/apps/cache"
"github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/base/internal/storage"
"github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/oauth"
"github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/oauth/ops/accesstokens"
"github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/oauth/ops/authority"
"github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/shared"
)
const (
// AuthorityPublicCloud is the default AAD authority host
AuthorityPublicCloud = "https://login.microsoftonline.com/common"
scopeSeparator = " "
)
// manager provides an internal cache. It is defined to allow faking the cache in tests.
// In production it's a *storage.Manager or *storage.PartitionedManager.
type manager interface {
cache.Serializer
Read(context.Context, authority.AuthParams) (storage.TokenResponse, error)
Write(authority.AuthParams, accesstokens.TokenResponse) (shared.Account, error)
}
// accountManager is a manager that also caches accounts. In production it's a *storage.Manager.
type accountManager interface {
manager
AllAccounts() []shared.Account
Account(homeAccountID string) shared.Account
RemoveAccount(account shared.Account, clientID string)
}
// AcquireTokenSilentParameters contains the parameters to acquire a token silently (from cache).
type AcquireTokenSilentParameters struct {
Scopes []string
Account shared.Account
RequestType accesstokens.AppType
Credential *accesstokens.Credential
IsAppCache bool
TenantID string
UserAssertion string
AuthorizationType authority.AuthorizeType
Claims string
}
// AcquireTokenAuthCodeParameters contains the parameters required to acquire an access token using the auth code flow.
// To use PKCE, set the CodeChallengeParameter.
// Code challenges are used to secure authorization code grants; for more information, visit
// https://tools.ietf.org/html/rfc7636.
type AcquireTokenAuthCodeParameters struct {
Scopes []string
Code string
Challenge string
Claims string
RedirectURI string
AppType accesstokens.AppType
Credential *accesstokens.Credential
TenantID string
}
type AcquireTokenOnBehalfOfParameters struct {
Scopes []string
Claims string
Credential *accesstokens.Credential
TenantID string
UserAssertion string
}
// AuthResult contains the results of one token acquisition operation in PublicClientApplication
// or ConfidentialClientApplication. For details see https://aka.ms/msal-net-authenticationresult
type AuthResult struct {
Account shared.Account
IDToken accesstokens.IDToken
AccessToken string
ExpiresOn time.Time
GrantedScopes []string
DeclinedScopes []string
}
// AuthResultFromStorage creates an AuthResult from a storage token response (which is generated from the cache).
func AuthResultFromStorage(storageTokenResponse storage.TokenResponse) (AuthResult, error) {
if err := storageTokenResponse.AccessToken.Validate(); err != nil {
return AuthResult{}, fmt.Errorf("problem with access token in StorageTokenResponse: %w", err)
}
account := storageTokenResponse.Account
accessToken := storageTokenResponse.AccessToken.Secret
grantedScopes := strings.Split(storageTokenResponse.AccessToken.Scopes, scopeSeparator)
// Checking if there was an ID token in the cache; this will throw an error in the case of confidential client applications.
var idToken accesstokens.IDToken
if !storageTokenResponse.IDToken.IsZero() {
err := idToken.UnmarshalJSON([]byte(storageTokenResponse.IDToken.Secret))
if err != nil {
return AuthResult{}, fmt.Errorf("problem decoding JWT token: %w", err)
}
}
return AuthResult{account, idToken, accessToken, storageTokenResponse.AccessToken.ExpiresOn.T, grantedScopes, nil}, nil
}
// NewAuthResult creates an AuthResult.
func NewAuthResult(tokenResponse accesstokens.TokenResponse, account shared.Account) (AuthResult, error) {
if len(tokenResponse.DeclinedScopes) > 0 {
return AuthResult{}, fmt.Errorf("token response failed because declined scopes are present: %s", strings.Join(tokenResponse.DeclinedScopes, ","))
}
return AuthResult{
Account: account,
IDToken: tokenResponse.IDToken,
AccessToken: tokenResponse.AccessToken,
ExpiresOn: tokenResponse.ExpiresOn.T,
GrantedScopes: tokenResponse.GrantedScopes.Slice,
}, nil
}
// Client is a base client that provides access to common methods and primatives that
// can be used by multiple clients.
type Client struct {
Token *oauth.Client
manager accountManager // *storage.Manager or fakeManager in tests
// pmanager is a partitioned cache for OBO authentication. *storage.PartitionedManager or fakeManager in tests
pmanager manager
AuthParams authority.AuthParams // DO NOT EVER MAKE THIS A POINTER! See "Note" in New().
cacheAccessor cache.ExportReplace
cacheAccessorMu *sync.RWMutex
}
// Option is an optional argument to the New constructor.
type Option func(c *Client) error
// WithCacheAccessor allows you to set some type of cache for storing authentication tokens.
func WithCacheAccessor(ca cache.ExportReplace) Option {
return func(c *Client) error {
if ca != nil {
c.cacheAccessor = ca
}
return nil
}
}
// WithClientCapabilities allows configuring one or more client capabilities such as "CP1"
func WithClientCapabilities(capabilities []string) Option {
return func(c *Client) error {
var err error
if len(capabilities) > 0 {
cc, err := authority.NewClientCapabilities(capabilities)
if err == nil {
c.AuthParams.Capabilities = cc
}
}
return err
}
}
// WithKnownAuthorityHosts specifies hosts Client shouldn't validate or request metadata for because they're known to the user
func WithKnownAuthorityHosts(hosts []string) Option {
return func(c *Client) error {
cp := make([]string, len(hosts))
copy(cp, hosts)
c.AuthParams.KnownAuthorityHosts = cp
return nil
}
}
// WithX5C specifies if x5c claim(public key of the certificate) should be sent to STS to enable Subject Name Issuer Authentication.
func WithX5C(sendX5C bool) Option {
return func(c *Client) error {
c.AuthParams.SendX5C = sendX5C
return nil
}
}
func WithRegionDetection(region string) Option {
return func(c *Client) error {
c.AuthParams.AuthorityInfo.Region = region
return nil
}
}
func WithInstanceDiscovery(instanceDiscoveryEnabled bool) Option {
return func(c *Client) error {
c.AuthParams.AuthorityInfo.ValidateAuthority = instanceDiscoveryEnabled
c.AuthParams.AuthorityInfo.InstanceDiscoveryDisabled = !instanceDiscoveryEnabled
return nil
}
}
// New is the constructor for Base.
func New(clientID string, authorityURI string, token *oauth.Client, options ...Option) (Client, error) {
//By default, validateAuthority is set to true and instanceDiscoveryDisabled is set to false
authInfo, err := authority.NewInfoFromAuthorityURI(authorityURI, true, false)
if err != nil {
return Client{}, err
}
authParams := authority.NewAuthParams(clientID, authInfo)
client := Client{ // Note: Hey, don't even THINK about making Base into *Base. See "design notes" in public.go and confidential.go
Token: token,
AuthParams: authParams,
cacheAccessorMu: &sync.RWMutex{},
manager: storage.New(token),
pmanager: storage.NewPartitionedManager(token),
}
for _, o := range options {
if err = o(&client); err != nil {
break
}
}
return client, err
}
// AuthCodeURL creates a URL used to acquire an authorization code.
func (b Client) AuthCodeURL(ctx context.Context, clientID, redirectURI string, scopes []string, authParams authority.AuthParams) (string, error) {
endpoints, err := b.Token.ResolveEndpoints(ctx, authParams.AuthorityInfo, "")
if err != nil {
return "", err
}
baseURL, err := url.Parse(endpoints.AuthorizationEndpoint)
if err != nil {
return "", err
}
claims, err := authParams.MergeCapabilitiesAndClaims()
if err != nil {
return "", err
}
v := url.Values{}
v.Add("client_id", clientID)
v.Add("response_type", "code")
v.Add("redirect_uri", redirectURI)
v.Add("scope", strings.Join(scopes, scopeSeparator))
if authParams.State != "" {
v.Add("state", authParams.State)
}
if claims != "" {
v.Add("claims", claims)
}
if authParams.CodeChallenge != "" {
v.Add("code_challenge", authParams.CodeChallenge)
}
if authParams.CodeChallengeMethod != "" {
v.Add("code_challenge_method", authParams.CodeChallengeMethod)
}
if authParams.LoginHint != "" {
v.Add("login_hint", authParams.LoginHint)
}
if authParams.Prompt != "" {
v.Add("prompt", authParams.Prompt)
}
if authParams.DomainHint != "" {
v.Add("domain_hint", authParams.DomainHint)
}
// There were left over from an implementation that didn't use any of these. We may
// need to add them later, but as of now aren't needed.
/*
if p.ResponseMode != "" {
urlParams.Add("response_mode", p.ResponseMode)
}
*/
baseURL.RawQuery = v.Encode()
return baseURL.String(), nil
}
func (b Client) AcquireTokenSilent(ctx context.Context, silent AcquireTokenSilentParameters) (AuthResult, error) {
ar := AuthResult{}
// when tenant == "", the caller didn't specify a tenant and WithTenant will choose the client's configured tenant
tenant := silent.TenantID
authParams, err := b.AuthParams.WithTenant(tenant)
if err != nil {
return ar, err
}
authParams.Scopes = silent.Scopes
authParams.HomeAccountID = silent.Account.HomeAccountID
authParams.AuthorizationType = silent.AuthorizationType
authParams.Claims = silent.Claims
authParams.UserAssertion = silent.UserAssertion
m := b.pmanager
if authParams.AuthorizationType != authority.ATOnBehalfOf {
authParams.AuthorizationType = authority.ATRefreshToken
m = b.manager
}
if b.cacheAccessor != nil {
key := authParams.CacheKey(silent.IsAppCache)
b.cacheAccessorMu.RLock()
err = b.cacheAccessor.Replace(ctx, m, cache.ReplaceHints{PartitionKey: key})
b.cacheAccessorMu.RUnlock()
}
if err != nil {
return ar, err
}
storageTokenResponse, err := m.Read(ctx, authParams)
if err != nil {
return ar, err
}
// ignore cached access tokens when given claims
if silent.Claims == "" {
ar, err = AuthResultFromStorage(storageTokenResponse)
if err == nil {
return ar, err
}
}
// redeem a cached refresh token, if available
if reflect.ValueOf(storageTokenResponse.RefreshToken).IsZero() {
return ar, errors.New("no token found")
}
var cc *accesstokens.Credential
if silent.RequestType == accesstokens.ATConfidential {
cc = silent.Credential
}
token, err := b.Token.Refresh(ctx, silent.RequestType, authParams, cc, storageTokenResponse.RefreshToken)
if err != nil {
return ar, err
}
return b.AuthResultFromToken(ctx, authParams, token, true)
}
func (b Client) AcquireTokenByAuthCode(ctx context.Context, authCodeParams AcquireTokenAuthCodeParameters) (AuthResult, error) {
authParams, err := b.AuthParams.WithTenant(authCodeParams.TenantID)
if err != nil {
return AuthResult{}, err
}
authParams.Claims = authCodeParams.Claims
authParams.Scopes = authCodeParams.Scopes
authParams.Redirecturi = authCodeParams.RedirectURI
authParams.AuthorizationType = authority.ATAuthCode
var cc *accesstokens.Credential
if authCodeParams.AppType == accesstokens.ATConfidential {
cc = authCodeParams.Credential
authParams.IsConfidentialClient = true
}
req, err := accesstokens.NewCodeChallengeRequest(authParams, authCodeParams.AppType, cc, authCodeParams.Code, authCodeParams.Challenge)
if err != nil {
return AuthResult{}, err
}
token, err := b.Token.AuthCode(ctx, req)
if err != nil {
return AuthResult{}, err
}
return b.AuthResultFromToken(ctx, authParams, token, true)
}
// AcquireTokenOnBehalfOf acquires a security token for an app using middle tier apps access token.
func (b Client) AcquireTokenOnBehalfOf(ctx context.Context, onBehalfOfParams AcquireTokenOnBehalfOfParameters) (AuthResult, error) {
var ar AuthResult
silentParameters := AcquireTokenSilentParameters{
Scopes: onBehalfOfParams.Scopes,
RequestType: accesstokens.ATConfidential,
Credential: onBehalfOfParams.Credential,
UserAssertion: onBehalfOfParams.UserAssertion,
AuthorizationType: authority.ATOnBehalfOf,
TenantID: onBehalfOfParams.TenantID,
Claims: onBehalfOfParams.Claims,
}
ar, err := b.AcquireTokenSilent(ctx, silentParameters)
if err == nil {
return ar, err
}
authParams, err := b.AuthParams.WithTenant(onBehalfOfParams.TenantID)
if err != nil {
return AuthResult{}, err
}
authParams.AuthorizationType = authority.ATOnBehalfOf
authParams.Claims = onBehalfOfParams.Claims
authParams.Scopes = onBehalfOfParams.Scopes
authParams.UserAssertion = onBehalfOfParams.UserAssertion
token, err := b.Token.OnBehalfOf(ctx, authParams, onBehalfOfParams.Credential)
if err == nil {
ar, err = b.AuthResultFromToken(ctx, authParams, token, true)
}
return ar, err
}
func (b Client) AuthResultFromToken(ctx context.Context, authParams authority.AuthParams, token accesstokens.TokenResponse, cacheWrite bool) (AuthResult, error) {
if !cacheWrite {
return NewAuthResult(token, shared.Account{})
}
var m manager = b.manager
if authParams.AuthorizationType == authority.ATOnBehalfOf {
m = b.pmanager
}
key := token.CacheKey(authParams)
if b.cacheAccessor != nil {
b.cacheAccessorMu.Lock()
defer b.cacheAccessorMu.Unlock()
err := b.cacheAccessor.Replace(ctx, m, cache.ReplaceHints{PartitionKey: key})
if err != nil {
return AuthResult{}, err
}
}
account, err := m.Write(authParams, token)
if err != nil {
return AuthResult{}, err
}
ar, err := NewAuthResult(token, account)
if err == nil && b.cacheAccessor != nil {
err = b.cacheAccessor.Export(ctx, b.manager, cache.ExportHints{PartitionKey: key})
}
return ar, err
}
func (b Client) AllAccounts(ctx context.Context) ([]shared.Account, error) {
if b.cacheAccessor != nil {
b.cacheAccessorMu.RLock()
defer b.cacheAccessorMu.RUnlock()
key := b.AuthParams.CacheKey(false)
err := b.cacheAccessor.Replace(ctx, b.manager, cache.ReplaceHints{PartitionKey: key})
if err != nil {
return nil, err
}
}
return b.manager.AllAccounts(), nil
}
func (b Client) Account(ctx context.Context, homeAccountID string) (shared.Account, error) {
if b.cacheAccessor != nil {
b.cacheAccessorMu.RLock()
defer b.cacheAccessorMu.RUnlock()
authParams := b.AuthParams // This is a copy, as we don't have a pointer receiver and .AuthParams is not a pointer.
authParams.AuthorizationType = authority.AccountByID
authParams.HomeAccountID = homeAccountID
key := b.AuthParams.CacheKey(false)
err := b.cacheAccessor.Replace(ctx, b.manager, cache.ReplaceHints{PartitionKey: key})
if err != nil {
return shared.Account{}, err
}
}
return b.manager.Account(homeAccountID), nil
}
// RemoveAccount removes all the ATs, RTs and IDTs from the cache associated with this account.
func (b Client) RemoveAccount(ctx context.Context, account shared.Account) error {
if b.cacheAccessor == nil {
b.manager.RemoveAccount(account, b.AuthParams.ClientID)
return nil
}
b.cacheAccessorMu.Lock()
defer b.cacheAccessorMu.Unlock()
key := b.AuthParams.CacheKey(false)
err := b.cacheAccessor.Replace(ctx, b.manager, cache.ReplaceHints{PartitionKey: key})
if err != nil {
return err
}
b.manager.RemoveAccount(account, b.AuthParams.ClientID)
return b.cacheAccessor.Export(ctx, b.manager, cache.ExportHints{PartitionKey: key})
}

View file

@ -0,0 +1,200 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
package storage
import (
"errors"
"fmt"
"reflect"
"strings"
"time"
internalTime "github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/json/types/time"
"github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/oauth/ops/accesstokens"
"github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/shared"
)
// Contract is the JSON structure that is written to any storage medium when serializing
// the internal cache. This design is shared between MSAL versions in many languages.
// This cannot be changed without design that includes other SDKs.
type Contract struct {
AccessTokens map[string]AccessToken `json:"AccessToken,omitempty"`
RefreshTokens map[string]accesstokens.RefreshToken `json:"RefreshToken,omitempty"`
IDTokens map[string]IDToken `json:"IdToken,omitempty"`
Accounts map[string]shared.Account `json:"Account,omitempty"`
AppMetaData map[string]AppMetaData `json:"AppMetadata,omitempty"`
AdditionalFields map[string]interface{}
}
// Contract is the JSON structure that is written to any storage medium when serializing
// the internal cache. This design is shared between MSAL versions in many languages.
// This cannot be changed without design that includes other SDKs.
type InMemoryContract struct {
AccessTokensPartition map[string]map[string]AccessToken
RefreshTokensPartition map[string]map[string]accesstokens.RefreshToken
IDTokensPartition map[string]map[string]IDToken
AccountsPartition map[string]map[string]shared.Account
AppMetaData map[string]AppMetaData
}
// NewContract is the constructor for Contract.
func NewInMemoryContract() *InMemoryContract {
return &InMemoryContract{
AccessTokensPartition: map[string]map[string]AccessToken{},
RefreshTokensPartition: map[string]map[string]accesstokens.RefreshToken{},
IDTokensPartition: map[string]map[string]IDToken{},
AccountsPartition: map[string]map[string]shared.Account{},
AppMetaData: map[string]AppMetaData{},
}
}
// NewContract is the constructor for Contract.
func NewContract() *Contract {
return &Contract{
AccessTokens: map[string]AccessToken{},
RefreshTokens: map[string]accesstokens.RefreshToken{},
IDTokens: map[string]IDToken{},
Accounts: map[string]shared.Account{},
AppMetaData: map[string]AppMetaData{},
AdditionalFields: map[string]interface{}{},
}
}
// AccessToken is the JSON representation of a MSAL access token for encoding to storage.
type AccessToken struct {
HomeAccountID string `json:"home_account_id,omitempty"`
Environment string `json:"environment,omitempty"`
Realm string `json:"realm,omitempty"`
CredentialType string `json:"credential_type,omitempty"`
ClientID string `json:"client_id,omitempty"`
Secret string `json:"secret,omitempty"`
Scopes string `json:"target,omitempty"`
ExpiresOn internalTime.Unix `json:"expires_on,omitempty"`
ExtendedExpiresOn internalTime.Unix `json:"extended_expires_on,omitempty"`
CachedAt internalTime.Unix `json:"cached_at,omitempty"`
UserAssertionHash string `json:"user_assertion_hash,omitempty"`
AdditionalFields map[string]interface{}
}
// NewAccessToken is the constructor for AccessToken.
func NewAccessToken(homeID, env, realm, clientID string, cachedAt, expiresOn, extendedExpiresOn time.Time, scopes, token string) AccessToken {
return AccessToken{
HomeAccountID: homeID,
Environment: env,
Realm: realm,
CredentialType: "AccessToken",
ClientID: clientID,
Secret: token,
Scopes: scopes,
CachedAt: internalTime.Unix{T: cachedAt.UTC()},
ExpiresOn: internalTime.Unix{T: expiresOn.UTC()},
ExtendedExpiresOn: internalTime.Unix{T: extendedExpiresOn.UTC()},
}
}
// Key outputs the key that can be used to uniquely look up this entry in a map.
func (a AccessToken) Key() string {
return strings.Join(
[]string{a.HomeAccountID, a.Environment, a.CredentialType, a.ClientID, a.Realm, a.Scopes},
shared.CacheKeySeparator,
)
}
// FakeValidate enables tests to fake access token validation
var FakeValidate func(AccessToken) error
// Validate validates that this AccessToken can be used.
func (a AccessToken) Validate() error {
if FakeValidate != nil {
return FakeValidate(a)
}
if a.CachedAt.T.After(time.Now()) {
return errors.New("access token isn't valid, it was cached at a future time")
}
if a.ExpiresOn.T.Before(time.Now().Add(5 * time.Minute)) {
return fmt.Errorf("access token is expired")
}
if a.CachedAt.T.IsZero() {
return fmt.Errorf("access token does not have CachedAt set")
}
return nil
}
// IDToken is the JSON representation of an MSAL id token for encoding to storage.
type IDToken struct {
HomeAccountID string `json:"home_account_id,omitempty"`
Environment string `json:"environment,omitempty"`
Realm string `json:"realm,omitempty"`
CredentialType string `json:"credential_type,omitempty"`
ClientID string `json:"client_id,omitempty"`
Secret string `json:"secret,omitempty"`
UserAssertionHash string `json:"user_assertion_hash,omitempty"`
AdditionalFields map[string]interface{}
}
// IsZero determines if IDToken is the zero value.
func (i IDToken) IsZero() bool {
v := reflect.ValueOf(i)
for i := 0; i < v.NumField(); i++ {
field := v.Field(i)
if !field.IsZero() {
switch field.Kind() {
case reflect.Map, reflect.Slice:
if field.Len() == 0 {
continue
}
}
return false
}
}
return true
}
// NewIDToken is the constructor for IDToken.
func NewIDToken(homeID, env, realm, clientID, idToken string) IDToken {
return IDToken{
HomeAccountID: homeID,
Environment: env,
Realm: realm,
CredentialType: "IDToken",
ClientID: clientID,
Secret: idToken,
}
}
// Key outputs the key that can be used to uniquely look up this entry in a map.
func (id IDToken) Key() string {
return strings.Join(
[]string{id.HomeAccountID, id.Environment, id.CredentialType, id.ClientID, id.Realm},
shared.CacheKeySeparator,
)
}
// AppMetaData is the JSON representation of application metadata for encoding to storage.
type AppMetaData struct {
FamilyID string `json:"family_id,omitempty"`
ClientID string `json:"client_id,omitempty"`
Environment string `json:"environment,omitempty"`
AdditionalFields map[string]interface{}
}
// NewAppMetaData is the constructor for AppMetaData.
func NewAppMetaData(familyID, clientID, environment string) AppMetaData {
return AppMetaData{
FamilyID: familyID,
ClientID: clientID,
Environment: environment,
}
}
// Key outputs the key that can be used to uniquely look up this entry in a map.
func (a AppMetaData) Key() string {
return strings.Join(
[]string{"AppMetaData", a.Environment, a.ClientID},
shared.CacheKeySeparator,
)
}

View file

@ -0,0 +1,436 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
package storage
import (
"context"
"errors"
"fmt"
"strings"
"sync"
"time"
"github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/json"
"github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/oauth"
"github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/oauth/ops/accesstokens"
"github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/oauth/ops/authority"
"github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/shared"
)
// PartitionedManager is a partitioned in-memory cache of access tokens, accounts and meta data.
type PartitionedManager struct {
contract *InMemoryContract
contractMu sync.RWMutex
requests aadInstanceDiscoveryer // *oauth.Token
aadCacheMu sync.RWMutex
aadCache map[string]authority.InstanceDiscoveryMetadata
}
// NewPartitionedManager is the constructor for PartitionedManager.
func NewPartitionedManager(requests *oauth.Client) *PartitionedManager {
m := &PartitionedManager{requests: requests, aadCache: make(map[string]authority.InstanceDiscoveryMetadata)}
m.contract = NewInMemoryContract()
return m
}
// Read reads a storage token from the cache if it exists.
func (m *PartitionedManager) Read(ctx context.Context, authParameters authority.AuthParams) (TokenResponse, error) {
tr := TokenResponse{}
realm := authParameters.AuthorityInfo.Tenant
clientID := authParameters.ClientID
scopes := authParameters.Scopes
// fetch metadata if instanceDiscovery is enabled
aliases := []string{authParameters.AuthorityInfo.Host}
if !authParameters.AuthorityInfo.InstanceDiscoveryDisabled {
metadata, err := m.getMetadataEntry(ctx, authParameters.AuthorityInfo)
if err != nil {
return TokenResponse{}, err
}
aliases = metadata.Aliases
}
userAssertionHash := authParameters.AssertionHash()
partitionKeyFromRequest := userAssertionHash
// errors returned by read* methods indicate a cache miss and are therefore non-fatal. We continue populating
// TokenResponse fields so that e.g. lack of an ID token doesn't prevent the caller from receiving a refresh token.
accessToken, err := m.readAccessToken(aliases, realm, clientID, userAssertionHash, scopes, partitionKeyFromRequest)
if err == nil {
tr.AccessToken = accessToken
}
idToken, err := m.readIDToken(aliases, realm, clientID, userAssertionHash, getPartitionKeyIDTokenRead(accessToken))
if err == nil {
tr.IDToken = idToken
}
if appMetadata, err := m.readAppMetaData(aliases, clientID); err == nil {
// we need the family ID to identify the correct refresh token, if any
familyID := appMetadata.FamilyID
refreshToken, err := m.readRefreshToken(aliases, familyID, clientID, userAssertionHash, partitionKeyFromRequest)
if err == nil {
tr.RefreshToken = refreshToken
}
}
account, err := m.readAccount(aliases, realm, userAssertionHash, idToken.HomeAccountID)
if err == nil {
tr.Account = account
}
return tr, nil
}
// Write writes a token response to the cache and returns the account information the token is stored with.
func (m *PartitionedManager) Write(authParameters authority.AuthParams, tokenResponse accesstokens.TokenResponse) (shared.Account, error) {
authParameters.HomeAccountID = tokenResponse.ClientInfo.HomeAccountID()
homeAccountID := authParameters.HomeAccountID
environment := authParameters.AuthorityInfo.Host
realm := authParameters.AuthorityInfo.Tenant
clientID := authParameters.ClientID
target := strings.Join(tokenResponse.GrantedScopes.Slice, scopeSeparator)
userAssertionHash := authParameters.AssertionHash()
cachedAt := time.Now()
var account shared.Account
if len(tokenResponse.RefreshToken) > 0 {
refreshToken := accesstokens.NewRefreshToken(homeAccountID, environment, clientID, tokenResponse.RefreshToken, tokenResponse.FamilyID)
if authParameters.AuthorizationType == authority.ATOnBehalfOf {
refreshToken.UserAssertionHash = userAssertionHash
}
if err := m.writeRefreshToken(refreshToken, getPartitionKeyRefreshToken(refreshToken)); err != nil {
return account, err
}
}
if len(tokenResponse.AccessToken) > 0 {
accessToken := NewAccessToken(
homeAccountID,
environment,
realm,
clientID,
cachedAt,
tokenResponse.ExpiresOn.T,
tokenResponse.ExtExpiresOn.T,
target,
tokenResponse.AccessToken,
)
if authParameters.AuthorizationType == authority.ATOnBehalfOf {
accessToken.UserAssertionHash = userAssertionHash // get Hash method on this
}
// Since we have a valid access token, cache it before moving on.
if err := accessToken.Validate(); err == nil {
if err := m.writeAccessToken(accessToken, getPartitionKeyAccessToken(accessToken)); err != nil {
return account, err
}
} else {
return shared.Account{}, err
}
}
idTokenJwt := tokenResponse.IDToken
if !idTokenJwt.IsZero() {
idToken := NewIDToken(homeAccountID, environment, realm, clientID, idTokenJwt.RawToken)
if authParameters.AuthorizationType == authority.ATOnBehalfOf {
idToken.UserAssertionHash = userAssertionHash
}
if err := m.writeIDToken(idToken, getPartitionKeyIDToken(idToken)); err != nil {
return shared.Account{}, err
}
localAccountID := idTokenJwt.LocalAccountID()
authorityType := authParameters.AuthorityInfo.AuthorityType
preferredUsername := idTokenJwt.UPN
if idTokenJwt.PreferredUsername != "" {
preferredUsername = idTokenJwt.PreferredUsername
}
account = shared.NewAccount(
homeAccountID,
environment,
realm,
localAccountID,
authorityType,
preferredUsername,
)
if authParameters.AuthorizationType == authority.ATOnBehalfOf {
account.UserAssertionHash = userAssertionHash
}
if err := m.writeAccount(account, getPartitionKeyAccount(account)); err != nil {
return shared.Account{}, err
}
}
AppMetaData := NewAppMetaData(tokenResponse.FamilyID, clientID, environment)
if err := m.writeAppMetaData(AppMetaData); err != nil {
return shared.Account{}, err
}
return account, nil
}
func (m *PartitionedManager) getMetadataEntry(ctx context.Context, authorityInfo authority.Info) (authority.InstanceDiscoveryMetadata, error) {
md, err := m.aadMetadataFromCache(ctx, authorityInfo)
if err != nil {
// not in the cache, retrieve it
md, err = m.aadMetadata(ctx, authorityInfo)
}
return md, err
}
func (m *PartitionedManager) aadMetadataFromCache(ctx context.Context, authorityInfo authority.Info) (authority.InstanceDiscoveryMetadata, error) {
m.aadCacheMu.RLock()
defer m.aadCacheMu.RUnlock()
metadata, ok := m.aadCache[authorityInfo.Host]
if ok {
return metadata, nil
}
return metadata, errors.New("not found")
}
func (m *PartitionedManager) aadMetadata(ctx context.Context, authorityInfo authority.Info) (authority.InstanceDiscoveryMetadata, error) {
discoveryResponse, err := m.requests.AADInstanceDiscovery(ctx, authorityInfo)
if err != nil {
return authority.InstanceDiscoveryMetadata{}, err
}
m.aadCacheMu.Lock()
defer m.aadCacheMu.Unlock()
for _, metadataEntry := range discoveryResponse.Metadata {
for _, aliasedAuthority := range metadataEntry.Aliases {
m.aadCache[aliasedAuthority] = metadataEntry
}
}
if _, ok := m.aadCache[authorityInfo.Host]; !ok {
m.aadCache[authorityInfo.Host] = authority.InstanceDiscoveryMetadata{
PreferredNetwork: authorityInfo.Host,
PreferredCache: authorityInfo.Host,
}
}
return m.aadCache[authorityInfo.Host], nil
}
func (m *PartitionedManager) readAccessToken(envAliases []string, realm, clientID, userAssertionHash string, scopes []string, partitionKey string) (AccessToken, error) {
m.contractMu.RLock()
defer m.contractMu.RUnlock()
if accessTokens, ok := m.contract.AccessTokensPartition[partitionKey]; ok {
// TODO: linear search (over a map no less) is slow for a large number (thousands) of tokens.
// this shows up as the dominating node in a profile. for real-world scenarios this likely isn't
// an issue, however if it does become a problem then we know where to look.
for _, at := range accessTokens {
if at.Realm == realm && at.ClientID == clientID && at.UserAssertionHash == userAssertionHash {
if checkAlias(at.Environment, envAliases) {
if isMatchingScopes(scopes, at.Scopes) {
return at, nil
}
}
}
}
}
return AccessToken{}, fmt.Errorf("access token not found")
}
func (m *PartitionedManager) writeAccessToken(accessToken AccessToken, partitionKey string) error {
m.contractMu.Lock()
defer m.contractMu.Unlock()
key := accessToken.Key()
if m.contract.AccessTokensPartition[partitionKey] == nil {
m.contract.AccessTokensPartition[partitionKey] = make(map[string]AccessToken)
}
m.contract.AccessTokensPartition[partitionKey][key] = accessToken
return nil
}
func matchFamilyRefreshTokenObo(rt accesstokens.RefreshToken, userAssertionHash string, envAliases []string) bool {
return rt.UserAssertionHash == userAssertionHash && checkAlias(rt.Environment, envAliases) && rt.FamilyID != ""
}
func matchClientIDRefreshTokenObo(rt accesstokens.RefreshToken, userAssertionHash string, envAliases []string, clientID string) bool {
return rt.UserAssertionHash == userAssertionHash && checkAlias(rt.Environment, envAliases) && rt.ClientID == clientID
}
func (m *PartitionedManager) readRefreshToken(envAliases []string, familyID, clientID, userAssertionHash, partitionKey string) (accesstokens.RefreshToken, error) {
byFamily := func(rt accesstokens.RefreshToken) bool {
return matchFamilyRefreshTokenObo(rt, userAssertionHash, envAliases)
}
byClient := func(rt accesstokens.RefreshToken) bool {
return matchClientIDRefreshTokenObo(rt, userAssertionHash, envAliases, clientID)
}
var matchers []func(rt accesstokens.RefreshToken) bool
if familyID == "" {
matchers = []func(rt accesstokens.RefreshToken) bool{
byClient, byFamily,
}
} else {
matchers = []func(rt accesstokens.RefreshToken) bool{
byFamily, byClient,
}
}
// TODO(keegan): All the tests here pass, but Bogdan says this is
// more complicated. I'm opening an issue for this to have him
// review the tests and suggest tests that would break this so
// we can re-write against good tests. His comments as follow:
// The algorithm is a bit more complex than this, I assume there are some tests covering everything. I would keep the order as is.
// The algorithm is:
// If application is NOT part of the family, search by client_ID
// If app is part of the family or if we DO NOT KNOW if it's part of the family, search by family ID, then by client_id (we will know if an app is part of the family after the first token response).
// https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/blob/311fe8b16e7c293462806f397e189a6aa1159769/src/client/Microsoft.Identity.Client/Internal/Requests/Silent/CacheSilentStrategy.cs#L95
m.contractMu.RLock()
defer m.contractMu.RUnlock()
for _, matcher := range matchers {
for _, rt := range m.contract.RefreshTokensPartition[partitionKey] {
if matcher(rt) {
return rt, nil
}
}
}
return accesstokens.RefreshToken{}, fmt.Errorf("refresh token not found")
}
func (m *PartitionedManager) writeRefreshToken(refreshToken accesstokens.RefreshToken, partitionKey string) error {
m.contractMu.Lock()
defer m.contractMu.Unlock()
key := refreshToken.Key()
if m.contract.AccessTokensPartition[partitionKey] == nil {
m.contract.RefreshTokensPartition[partitionKey] = make(map[string]accesstokens.RefreshToken)
}
m.contract.RefreshTokensPartition[partitionKey][key] = refreshToken
return nil
}
func (m *PartitionedManager) readIDToken(envAliases []string, realm, clientID, userAssertionHash, partitionKey string) (IDToken, error) {
m.contractMu.RLock()
defer m.contractMu.RUnlock()
for _, idt := range m.contract.IDTokensPartition[partitionKey] {
if idt.Realm == realm && idt.ClientID == clientID && idt.UserAssertionHash == userAssertionHash {
if checkAlias(idt.Environment, envAliases) {
return idt, nil
}
}
}
return IDToken{}, fmt.Errorf("token not found")
}
func (m *PartitionedManager) writeIDToken(idToken IDToken, partitionKey string) error {
key := idToken.Key()
m.contractMu.Lock()
defer m.contractMu.Unlock()
if m.contract.IDTokensPartition[partitionKey] == nil {
m.contract.IDTokensPartition[partitionKey] = make(map[string]IDToken)
}
m.contract.IDTokensPartition[partitionKey][key] = idToken
return nil
}
func (m *PartitionedManager) readAccount(envAliases []string, realm, UserAssertionHash, partitionKey string) (shared.Account, error) {
m.contractMu.RLock()
defer m.contractMu.RUnlock()
// You might ask why, if cache.Accounts is a map, we would loop through all of these instead of using a key.
// We only use a map because the storage contract shared between all language implementations says use a map.
// We can't change that. The other is because the keys are made using a specific "env", but here we are allowing
// a match in multiple envs (envAlias). That means we either need to hash each possible keyand do the lookup
// or just statically check. Since the design is to have a storage.Manager per user, the amount of keys stored
// is really low (say 2). Each hash is more expensive than the entire iteration.
for _, acc := range m.contract.AccountsPartition[partitionKey] {
if checkAlias(acc.Environment, envAliases) && acc.UserAssertionHash == UserAssertionHash && acc.Realm == realm {
return acc, nil
}
}
return shared.Account{}, fmt.Errorf("account not found")
}
func (m *PartitionedManager) writeAccount(account shared.Account, partitionKey string) error {
key := account.Key()
m.contractMu.Lock()
defer m.contractMu.Unlock()
if m.contract.AccountsPartition[partitionKey] == nil {
m.contract.AccountsPartition[partitionKey] = make(map[string]shared.Account)
}
m.contract.AccountsPartition[partitionKey][key] = account
return nil
}
func (m *PartitionedManager) readAppMetaData(envAliases []string, clientID string) (AppMetaData, error) {
m.contractMu.RLock()
defer m.contractMu.RUnlock()
for _, app := range m.contract.AppMetaData {
if checkAlias(app.Environment, envAliases) && app.ClientID == clientID {
return app, nil
}
}
return AppMetaData{}, fmt.Errorf("not found")
}
func (m *PartitionedManager) writeAppMetaData(AppMetaData AppMetaData) error {
key := AppMetaData.Key()
m.contractMu.Lock()
defer m.contractMu.Unlock()
m.contract.AppMetaData[key] = AppMetaData
return nil
}
// update updates the internal cache object. This is for use in tests, other uses are not
// supported.
func (m *PartitionedManager) update(cache *InMemoryContract) {
m.contractMu.Lock()
defer m.contractMu.Unlock()
m.contract = cache
}
// Marshal implements cache.Marshaler.
func (m *PartitionedManager) Marshal() ([]byte, error) {
return json.Marshal(m.contract)
}
// Unmarshal implements cache.Unmarshaler.
func (m *PartitionedManager) Unmarshal(b []byte) error {
m.contractMu.Lock()
defer m.contractMu.Unlock()
contract := NewInMemoryContract()
err := json.Unmarshal(b, contract)
if err != nil {
return err
}
m.contract = contract
return nil
}
func getPartitionKeyAccessToken(item AccessToken) string {
if item.UserAssertionHash != "" {
return item.UserAssertionHash
}
return item.HomeAccountID
}
func getPartitionKeyRefreshToken(item accesstokens.RefreshToken) string {
if item.UserAssertionHash != "" {
return item.UserAssertionHash
}
return item.HomeAccountID
}
func getPartitionKeyIDToken(item IDToken) string {
return item.HomeAccountID
}
func getPartitionKeyAccount(item shared.Account) string {
return item.HomeAccountID
}
func getPartitionKeyIDTokenRead(item AccessToken) string {
return item.HomeAccountID
}

View file

@ -0,0 +1,517 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
// Package storage holds all cached token information for MSAL. This storage can be
// augmented with third-party extensions to provide persistent storage. In that case,
// reads and writes in upper packages will call Marshal() to take the entire in-memory
// representation and write it to storage and Unmarshal() to update the entire in-memory
// storage with what was in the persistent storage. The persistent storage can only be
// accessed in this way because multiple MSAL clients written in multiple languages can
// access the same storage and must adhere to the same method that was defined
// previously.
package storage
import (
"context"
"errors"
"fmt"
"strings"
"sync"
"time"
"github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/json"
"github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/oauth"
"github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/oauth/ops/accesstokens"
"github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/oauth/ops/authority"
"github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/shared"
)
// aadInstanceDiscoveryer allows faking in tests.
// It is implemented in production by ops/authority.Client
type aadInstanceDiscoveryer interface {
AADInstanceDiscovery(ctx context.Context, authorityInfo authority.Info) (authority.InstanceDiscoveryResponse, error)
}
// TokenResponse mimics a token response that was pulled from the cache.
type TokenResponse struct {
RefreshToken accesstokens.RefreshToken
IDToken IDToken // *Credential
AccessToken AccessToken
Account shared.Account
}
// Manager is an in-memory cache of access tokens, accounts and meta data. This data is
// updated on read/write calls. Unmarshal() replaces all data stored here with whatever
// was given to it on each call.
type Manager struct {
contract *Contract
contractMu sync.RWMutex
requests aadInstanceDiscoveryer // *oauth.Token
aadCacheMu sync.RWMutex
aadCache map[string]authority.InstanceDiscoveryMetadata
}
// New is the constructor for Manager.
func New(requests *oauth.Client) *Manager {
m := &Manager{requests: requests, aadCache: make(map[string]authority.InstanceDiscoveryMetadata)}
m.contract = NewContract()
return m
}
func checkAlias(alias string, aliases []string) bool {
for _, v := range aliases {
if alias == v {
return true
}
}
return false
}
func isMatchingScopes(scopesOne []string, scopesTwo string) bool {
newScopesTwo := strings.Split(scopesTwo, scopeSeparator)
scopeCounter := 0
for _, scope := range scopesOne {
for _, otherScope := range newScopesTwo {
if strings.EqualFold(scope, otherScope) {
scopeCounter++
continue
}
}
}
return scopeCounter == len(scopesOne)
}
// Read reads a storage token from the cache if it exists.
func (m *Manager) Read(ctx context.Context, authParameters authority.AuthParams) (TokenResponse, error) {
tr := TokenResponse{}
homeAccountID := authParameters.HomeAccountID
realm := authParameters.AuthorityInfo.Tenant
clientID := authParameters.ClientID
scopes := authParameters.Scopes
// fetch metadata if instanceDiscovery is enabled
aliases := []string{authParameters.AuthorityInfo.Host}
if !authParameters.AuthorityInfo.InstanceDiscoveryDisabled {
metadata, err := m.getMetadataEntry(ctx, authParameters.AuthorityInfo)
if err != nil {
return TokenResponse{}, err
}
aliases = metadata.Aliases
}
accessToken := m.readAccessToken(homeAccountID, aliases, realm, clientID, scopes)
tr.AccessToken = accessToken
if homeAccountID == "" {
// caller didn't specify a user, so there's no reason to search for an ID or refresh token
return tr, nil
}
// errors returned by read* methods indicate a cache miss and are therefore non-fatal. We continue populating
// TokenResponse fields so that e.g. lack of an ID token doesn't prevent the caller from receiving a refresh token.
idToken, err := m.readIDToken(homeAccountID, aliases, realm, clientID)
if err == nil {
tr.IDToken = idToken
}
if appMetadata, err := m.readAppMetaData(aliases, clientID); err == nil {
// we need the family ID to identify the correct refresh token, if any
familyID := appMetadata.FamilyID
refreshToken, err := m.readRefreshToken(homeAccountID, aliases, familyID, clientID)
if err == nil {
tr.RefreshToken = refreshToken
}
}
account, err := m.readAccount(homeAccountID, aliases, realm)
if err == nil {
tr.Account = account
}
return tr, nil
}
const scopeSeparator = " "
// Write writes a token response to the cache and returns the account information the token is stored with.
func (m *Manager) Write(authParameters authority.AuthParams, tokenResponse accesstokens.TokenResponse) (shared.Account, error) {
authParameters.HomeAccountID = tokenResponse.ClientInfo.HomeAccountID()
homeAccountID := authParameters.HomeAccountID
environment := authParameters.AuthorityInfo.Host
realm := authParameters.AuthorityInfo.Tenant
clientID := authParameters.ClientID
target := strings.Join(tokenResponse.GrantedScopes.Slice, scopeSeparator)
cachedAt := time.Now()
var account shared.Account
if len(tokenResponse.RefreshToken) > 0 {
refreshToken := accesstokens.NewRefreshToken(homeAccountID, environment, clientID, tokenResponse.RefreshToken, tokenResponse.FamilyID)
if err := m.writeRefreshToken(refreshToken); err != nil {
return account, err
}
}
if len(tokenResponse.AccessToken) > 0 {
accessToken := NewAccessToken(
homeAccountID,
environment,
realm,
clientID,
cachedAt,
tokenResponse.ExpiresOn.T,
tokenResponse.ExtExpiresOn.T,
target,
tokenResponse.AccessToken,
)
// Since we have a valid access token, cache it before moving on.
if err := accessToken.Validate(); err == nil {
if err := m.writeAccessToken(accessToken); err != nil {
return account, err
}
}
}
idTokenJwt := tokenResponse.IDToken
if !idTokenJwt.IsZero() {
idToken := NewIDToken(homeAccountID, environment, realm, clientID, idTokenJwt.RawToken)
if err := m.writeIDToken(idToken); err != nil {
return shared.Account{}, err
}
localAccountID := idTokenJwt.LocalAccountID()
authorityType := authParameters.AuthorityInfo.AuthorityType
preferredUsername := idTokenJwt.UPN
if idTokenJwt.PreferredUsername != "" {
preferredUsername = idTokenJwt.PreferredUsername
}
account = shared.NewAccount(
homeAccountID,
environment,
realm,
localAccountID,
authorityType,
preferredUsername,
)
if err := m.writeAccount(account); err != nil {
return shared.Account{}, err
}
}
AppMetaData := NewAppMetaData(tokenResponse.FamilyID, clientID, environment)
if err := m.writeAppMetaData(AppMetaData); err != nil {
return shared.Account{}, err
}
return account, nil
}
func (m *Manager) getMetadataEntry(ctx context.Context, authorityInfo authority.Info) (authority.InstanceDiscoveryMetadata, error) {
md, err := m.aadMetadataFromCache(ctx, authorityInfo)
if err != nil {
// not in the cache, retrieve it
md, err = m.aadMetadata(ctx, authorityInfo)
}
return md, err
}
func (m *Manager) aadMetadataFromCache(ctx context.Context, authorityInfo authority.Info) (authority.InstanceDiscoveryMetadata, error) {
m.aadCacheMu.RLock()
defer m.aadCacheMu.RUnlock()
metadata, ok := m.aadCache[authorityInfo.Host]
if ok {
return metadata, nil
}
return metadata, errors.New("not found")
}
func (m *Manager) aadMetadata(ctx context.Context, authorityInfo authority.Info) (authority.InstanceDiscoveryMetadata, error) {
m.aadCacheMu.Lock()
defer m.aadCacheMu.Unlock()
discoveryResponse, err := m.requests.AADInstanceDiscovery(ctx, authorityInfo)
if err != nil {
return authority.InstanceDiscoveryMetadata{}, err
}
for _, metadataEntry := range discoveryResponse.Metadata {
for _, aliasedAuthority := range metadataEntry.Aliases {
m.aadCache[aliasedAuthority] = metadataEntry
}
}
if _, ok := m.aadCache[authorityInfo.Host]; !ok {
m.aadCache[authorityInfo.Host] = authority.InstanceDiscoveryMetadata{
PreferredNetwork: authorityInfo.Host,
PreferredCache: authorityInfo.Host,
}
}
return m.aadCache[authorityInfo.Host], nil
}
func (m *Manager) readAccessToken(homeID string, envAliases []string, realm, clientID string, scopes []string) AccessToken {
m.contractMu.RLock()
defer m.contractMu.RUnlock()
// TODO: linear search (over a map no less) is slow for a large number (thousands) of tokens.
// this shows up as the dominating node in a profile. for real-world scenarios this likely isn't
// an issue, however if it does become a problem then we know where to look.
for _, at := range m.contract.AccessTokens {
if at.HomeAccountID == homeID && at.Realm == realm && at.ClientID == clientID {
if checkAlias(at.Environment, envAliases) {
if isMatchingScopes(scopes, at.Scopes) {
return at
}
}
}
}
return AccessToken{}
}
func (m *Manager) writeAccessToken(accessToken AccessToken) error {
m.contractMu.Lock()
defer m.contractMu.Unlock()
key := accessToken.Key()
m.contract.AccessTokens[key] = accessToken
return nil
}
func (m *Manager) readRefreshToken(homeID string, envAliases []string, familyID, clientID string) (accesstokens.RefreshToken, error) {
byFamily := func(rt accesstokens.RefreshToken) bool {
return matchFamilyRefreshToken(rt, homeID, envAliases)
}
byClient := func(rt accesstokens.RefreshToken) bool {
return matchClientIDRefreshToken(rt, homeID, envAliases, clientID)
}
var matchers []func(rt accesstokens.RefreshToken) bool
if familyID == "" {
matchers = []func(rt accesstokens.RefreshToken) bool{
byClient, byFamily,
}
} else {
matchers = []func(rt accesstokens.RefreshToken) bool{
byFamily, byClient,
}
}
// TODO(keegan): All the tests here pass, but Bogdan says this is
// more complicated. I'm opening an issue for this to have him
// review the tests and suggest tests that would break this so
// we can re-write against good tests. His comments as follow:
// The algorithm is a bit more complex than this, I assume there are some tests covering everything. I would keep the order as is.
// The algorithm is:
// If application is NOT part of the family, search by client_ID
// If app is part of the family or if we DO NOT KNOW if it's part of the family, search by family ID, then by client_id (we will know if an app is part of the family after the first token response).
// https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/blob/311fe8b16e7c293462806f397e189a6aa1159769/src/client/Microsoft.Identity.Client/Internal/Requests/Silent/CacheSilentStrategy.cs#L95
m.contractMu.RLock()
defer m.contractMu.RUnlock()
for _, matcher := range matchers {
for _, rt := range m.contract.RefreshTokens {
if matcher(rt) {
return rt, nil
}
}
}
return accesstokens.RefreshToken{}, fmt.Errorf("refresh token not found")
}
func matchFamilyRefreshToken(rt accesstokens.RefreshToken, homeID string, envAliases []string) bool {
return rt.HomeAccountID == homeID && checkAlias(rt.Environment, envAliases) && rt.FamilyID != ""
}
func matchClientIDRefreshToken(rt accesstokens.RefreshToken, homeID string, envAliases []string, clientID string) bool {
return rt.HomeAccountID == homeID && checkAlias(rt.Environment, envAliases) && rt.ClientID == clientID
}
func (m *Manager) writeRefreshToken(refreshToken accesstokens.RefreshToken) error {
key := refreshToken.Key()
m.contractMu.Lock()
defer m.contractMu.Unlock()
m.contract.RefreshTokens[key] = refreshToken
return nil
}
func (m *Manager) readIDToken(homeID string, envAliases []string, realm, clientID string) (IDToken, error) {
m.contractMu.RLock()
defer m.contractMu.RUnlock()
for _, idt := range m.contract.IDTokens {
if idt.HomeAccountID == homeID && idt.Realm == realm && idt.ClientID == clientID {
if checkAlias(idt.Environment, envAliases) {
return idt, nil
}
}
}
return IDToken{}, fmt.Errorf("token not found")
}
func (m *Manager) writeIDToken(idToken IDToken) error {
key := idToken.Key()
m.contractMu.Lock()
defer m.contractMu.Unlock()
m.contract.IDTokens[key] = idToken
return nil
}
func (m *Manager) AllAccounts() []shared.Account {
m.contractMu.RLock()
defer m.contractMu.RUnlock()
var accounts []shared.Account
for _, v := range m.contract.Accounts {
accounts = append(accounts, v)
}
return accounts
}
func (m *Manager) Account(homeAccountID string) shared.Account {
m.contractMu.RLock()
defer m.contractMu.RUnlock()
for _, v := range m.contract.Accounts {
if v.HomeAccountID == homeAccountID {
return v
}
}
return shared.Account{}
}
func (m *Manager) readAccount(homeAccountID string, envAliases []string, realm string) (shared.Account, error) {
m.contractMu.RLock()
defer m.contractMu.RUnlock()
// You might ask why, if cache.Accounts is a map, we would loop through all of these instead of using a key.
// We only use a map because the storage contract shared between all language implementations says use a map.
// We can't change that. The other is because the keys are made using a specific "env", but here we are allowing
// a match in multiple envs (envAlias). That means we either need to hash each possible keyand do the lookup
// or just statically check. Since the design is to have a storage.Manager per user, the amount of keys stored
// is really low (say 2). Each hash is more expensive than the entire iteration.
for _, acc := range m.contract.Accounts {
if acc.HomeAccountID == homeAccountID && checkAlias(acc.Environment, envAliases) && acc.Realm == realm {
return acc, nil
}
}
return shared.Account{}, fmt.Errorf("account not found")
}
func (m *Manager) writeAccount(account shared.Account) error {
key := account.Key()
m.contractMu.Lock()
defer m.contractMu.Unlock()
m.contract.Accounts[key] = account
return nil
}
func (m *Manager) readAppMetaData(envAliases []string, clientID string) (AppMetaData, error) {
m.contractMu.RLock()
defer m.contractMu.RUnlock()
for _, app := range m.contract.AppMetaData {
if checkAlias(app.Environment, envAliases) && app.ClientID == clientID {
return app, nil
}
}
return AppMetaData{}, fmt.Errorf("not found")
}
func (m *Manager) writeAppMetaData(AppMetaData AppMetaData) error {
key := AppMetaData.Key()
m.contractMu.Lock()
defer m.contractMu.Unlock()
m.contract.AppMetaData[key] = AppMetaData
return nil
}
// RemoveAccount removes all the associated ATs, RTs and IDTs from the cache associated with this account.
func (m *Manager) RemoveAccount(account shared.Account, clientID string) {
m.removeRefreshTokens(account.HomeAccountID, account.Environment, clientID)
m.removeAccessTokens(account.HomeAccountID, account.Environment)
m.removeIDTokens(account.HomeAccountID, account.Environment)
m.removeAccounts(account.HomeAccountID, account.Environment)
}
func (m *Manager) removeRefreshTokens(homeID string, env string, clientID string) {
m.contractMu.Lock()
defer m.contractMu.Unlock()
for key, rt := range m.contract.RefreshTokens {
// Check for RTs associated with the account.
if rt.HomeAccountID == homeID && rt.Environment == env {
// Do RT's app ownership check as a precaution, in case family apps
// and 3rd-party apps share same token cache, although they should not.
if rt.ClientID == clientID || rt.FamilyID != "" {
delete(m.contract.RefreshTokens, key)
}
}
}
}
func (m *Manager) removeAccessTokens(homeID string, env string) {
m.contractMu.Lock()
defer m.contractMu.Unlock()
for key, at := range m.contract.AccessTokens {
// Remove AT's associated with the account
if at.HomeAccountID == homeID && at.Environment == env {
// # To avoid the complexity of locating sibling family app's AT, we skip AT's app ownership check.
// It means ATs for other apps will also be removed, it is OK because:
// non-family apps are not supposed to share token cache to begin with;
// Even if it happens, we keep other app's RT already, so SSO still works.
delete(m.contract.AccessTokens, key)
}
}
}
func (m *Manager) removeIDTokens(homeID string, env string) {
m.contractMu.Lock()
defer m.contractMu.Unlock()
for key, idt := range m.contract.IDTokens {
// Remove ID tokens associated with the account.
if idt.HomeAccountID == homeID && idt.Environment == env {
delete(m.contract.IDTokens, key)
}
}
}
func (m *Manager) removeAccounts(homeID string, env string) {
m.contractMu.Lock()
defer m.contractMu.Unlock()
for key, acc := range m.contract.Accounts {
// Remove the specified account.
if acc.HomeAccountID == homeID && acc.Environment == env {
delete(m.contract.Accounts, key)
}
}
}
// update updates the internal cache object. This is for use in tests, other uses are not
// supported.
func (m *Manager) update(cache *Contract) {
m.contractMu.Lock()
defer m.contractMu.Unlock()
m.contract = cache
}
// Marshal implements cache.Marshaler.
func (m *Manager) Marshal() ([]byte, error) {
m.contractMu.RLock()
defer m.contractMu.RUnlock()
return json.Marshal(m.contract)
}
// Unmarshal implements cache.Unmarshaler.
func (m *Manager) Unmarshal(b []byte) error {
m.contractMu.Lock()
defer m.contractMu.Unlock()
contract := NewContract()
err := json.Unmarshal(b, contract)
if err != nil {
return err
}
m.contract = contract
return nil
}

View file

@ -0,0 +1,56 @@
{
"Account": {
"uid.utid-login.windows.net-contoso": {
"username": "John Doe",
"local_account_id": "object1234",
"realm": "contoso",
"environment": "login.windows.net",
"home_account_id": "uid.utid",
"authority_type": "MSSTS"
}
},
"RefreshToken": {
"uid.utid-login.windows.net-refreshtoken-my_client_id--s2 s1 s3": {
"target": "s2 s1 s3",
"environment": "login.windows.net",
"credential_type": "RefreshToken",
"secret": "a refresh token",
"client_id": "my_client_id",
"home_account_id": "uid.utid"
}
},
"AccessToken": {
"an-entry": {
"foo": "bar"
},
"uid.utid-login.windows.net-accesstoken-my_client_id-contoso-s2 s1 s3": {
"environment": "login.windows.net",
"credential_type": "AccessToken",
"secret": "an access token",
"realm": "contoso",
"target": "s2 s1 s3",
"client_id": "my_client_id",
"cached_at": "1000",
"home_account_id": "uid.utid",
"extended_expires_on": "4600",
"expires_on": "4600"
}
},
"IdToken": {
"uid.utid-login.windows.net-idtoken-my_client_id-contoso-": {
"realm": "contoso",
"environment": "login.windows.net",
"credential_type": "IdToken",
"secret": "header.eyJvaWQiOiAib2JqZWN0MTIzNCIsICJwcmVmZXJyZWRfdXNlcm5hbWUiOiAiSm9obiBEb2UiLCAic3ViIjogInN1YiJ9.signature",
"client_id": "my_client_id",
"home_account_id": "uid.utid"
}
},
"unknownEntity": {"field1":"1","field2":"whats"},
"AppMetadata": {
"AppMetadata-login.windows.net-my_client_id": {
"environment": "login.windows.net",
"client_id": "my_client_id"
}
}
}

View file

@ -0,0 +1,34 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
// package exported contains internal types that are re-exported from a public package
package exported
// AssertionRequestOptions has information required to generate a client assertion
type AssertionRequestOptions struct {
// ClientID identifies the application for which an assertion is requested. Used as the assertion's "iss" and "sub" claims.
ClientID string
// TokenEndpoint is the intended token endpoint. Used as the assertion's "aud" claim.
TokenEndpoint string
}
// TokenProviderParameters is the authentication parameters passed to token providers
type TokenProviderParameters struct {
// Claims contains any additional claims requested for the token
Claims string
// CorrelationID of the authentication request
CorrelationID string
// Scopes requested for the token
Scopes []string
// TenantID identifies the tenant in which to authenticate
TenantID string
}
// TokenProviderResult is the authentication result returned by custom token providers
type TokenProviderResult struct {
// AccessToken is the requested token
AccessToken string
// ExpiresInSeconds is the lifetime of the token in seconds
ExpiresInSeconds int
}

View file

@ -0,0 +1,140 @@
# JSON Package Design
Author: John Doak(jdoak@microsoft.com)
## Why?
This project needs a special type of marshal/unmarshal not directly supported
by the encoding/json package.
The need revolves around a few key wants/needs:
- unmarshal and marshal structs representing JSON messages
- fields in the messgage not in the struct must be maintained when unmarshalled
- those same fields must be marshalled back when encoded again
The initial version used map[string]interface{} to put in the keys that
were known and then any other keys were put into a field called AdditionalFields.
This has a few negatives:
- Dual marshaling/unmarshalling is required
- Adding a struct field requires manually adding a key by name to be encoded/decoded from the map (which is a loosely coupled construct), which can lead to bugs that aren't detected or have bad side effects
- Tests can become quickly disconnected if those keys aren't put
in tests as well. So you think you have support working, but you
don't. Existing tests were found that didn't test the marshalling output.
- There is no enforcement that if AdditionalFields is required on one struct, it should be on all containers
that don't have custom marshal/unmarshal.
This package aims to support our needs by providing custom Marshal()/Unmarshal() functions.
This prevents all the negatives in the initial solution listed above. However, it does add its own negative:
- Custom encoding/decoding via reflection is messy (as can be seen in encoding/json itself)
Go proverb: Reflection is never clear
Suggested reading: https://blog.golang.org/laws-of-reflection
## Important design decisions
- We don't want to understand all JSON decoding rules
- We don't want to deal with all the quoting, commas, etc on decode
- Need support for json.Marshaler/Unmarshaler, so we can support types like time.Time
- If struct does not implement json.Unmarshaler, it must have AdditionalFields defined
- We only support root level objects that are \*struct or struct
To faciliate these goals, we will utilize the json.Encoder and json.Decoder.
They provide streaming processing (efficient) and return errors on bad JSON.
Support for json.Marshaler/Unmarshaler allows for us to use non-basic types
that must be specially encoded/decoded (like time.Time objects).
We don't support types that can't customer unmarshal or have AdditionalFields
in order to prevent future devs from forgetting that important field and
generating bad return values.
Support for root level objects of \*struct or struct simply acknowledges the
fact that this is designed only for the purposes listed in the Introduction.
Outside that (like encoding a lone number) should be done with the
regular json package (as it will not have additional fields).
We don't support a few things on json supported reference types and structs:
- \*map: no need for pointers to maps
- \*slice: no need for pointers to slices
- any further pointers on struct after \*struct
There should never be a need for this in Go.
## Design
## State Machines
This uses state machine designs that based upon the Rob Pike talk on
lexers and parsers: https://www.youtube.com/watch?v=HxaD_trXwRE
This is the most common pattern for state machines in Go and
the model to follow closesly when dealing with streaming
processing of textual data.
Our state machines are based on the type:
```go
type stateFn func() (stateFn, error)
```
The state machine itself is simply a struct that has methods that
satisfy stateFn.
Our state machines have a few standard calls
- run(): runs the state machine
- start(): always the first stateFn to be called
All state machines have the following logic:
* run() is called
* start() is called and returns the next stateFn or error
* stateFn is called
- If returned stateFn(next state) is non-nil, call it
- If error is non-nil, run() returns the error
- If stateFn == nil and err == nil, run() return err == nil
## Supporting types
Marshalling/Unmarshalling must support(within top level struct):
- struct
- \*struct
- []struct
- []\*struct
- []map[string]structContainer
- [][]structContainer
**Term note:** structContainer == type that has a struct or \*struct inside it
We specifically do not support []interface or map[string]interface
where the interface value would hold some value with a struct in it.
Those will still marshal/unmarshal, but without support for
AdditionalFields.
## Marshalling
The marshalling design will be based around a statemachine design.
The basic logic is as follows:
* If struct has custom marshaller, call it and return
* If struct has field "AdditionalFields", it must be a map[string]interface{}
* If struct does not have "AdditionalFields", give an error
* Get struct tag detailing json names to go names, create mapping
* For each public field name
- Write field name out
- If field value is a struct, recursively call our state machine
- Otherwise, use the json.Encoder to write out the value
## Unmarshalling
The unmarshalling desin is also based around a statemachine design. The
basic logic is as follows:
* If struct has custom marhaller, call it
* If struct has field "AdditionalFields", it must be a map[string]interface{}
* Get struct tag detailing json names to go names, create mapping
* For each key found
- If key exists,
- If value is basic type, extract value into struct field using Decoder
- If value is struct type, recursively call statemachine
- If key doesn't exist, add it to AdditionalFields if it exists using Decoder

View file

@ -0,0 +1,184 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
// Package json provide functions for marshalling an unmarshalling types to JSON. These functions are meant to
// be utilized inside of structs that implement json.Unmarshaler and json.Marshaler interfaces.
// This package provides the additional functionality of writing fields that are not in the struct when marshalling
// to a field called AdditionalFields if that field exists and is a map[string]interface{}.
// When marshalling, if the struct has all the same prerequisites, it will uses the keys in AdditionalFields as
// extra fields. This package uses encoding/json underneath.
package json
import (
"bytes"
"encoding/json"
"fmt"
"reflect"
"strings"
)
const addField = "AdditionalFields"
const (
marshalJSON = "MarshalJSON"
unmarshalJSON = "UnmarshalJSON"
)
var (
leftBrace = []byte("{")[0]
rightBrace = []byte("}")[0]
comma = []byte(",")[0]
leftParen = []byte("[")[0]
rightParen = []byte("]")[0]
)
var mapStrInterType = reflect.TypeOf(map[string]interface{}{})
// stateFn defines a state machine function. This will be used in all state
// machines in this package.
type stateFn func() (stateFn, error)
// Marshal is used to marshal a type into its JSON representation. It
// wraps the stdlib calls in order to marshal a struct or *struct so
// that a field called "AdditionalFields" of type map[string]interface{}
// with "-" used inside struct tag `json:"-"` can be marshalled as if
// they were fields within the struct.
func Marshal(i interface{}) ([]byte, error) {
buff := bytes.Buffer{}
enc := json.NewEncoder(&buff)
enc.SetEscapeHTML(false)
enc.SetIndent("", "")
v := reflect.ValueOf(i)
if v.Kind() != reflect.Ptr && v.CanAddr() {
v = v.Addr()
}
err := marshalStruct(v, &buff, enc)
if err != nil {
return nil, err
}
return buff.Bytes(), nil
}
// Unmarshal unmarshals a []byte representing JSON into i, which must be a *struct. In addition, if the struct has
// a field called AdditionalFields of type map[string]interface{}, JSON data representing fields not in the struct
// will be written as key/value pairs to AdditionalFields.
func Unmarshal(b []byte, i interface{}) error {
if len(b) == 0 {
return nil
}
jdec := json.NewDecoder(bytes.NewBuffer(b))
jdec.UseNumber()
return unmarshalStruct(jdec, i)
}
// MarshalRaw marshals i into a json.RawMessage. If I cannot be marshalled,
// this will panic. This is exposed to help test AdditionalField values
// which are stored as json.RawMessage.
func MarshalRaw(i interface{}) json.RawMessage {
b, err := json.Marshal(i)
if err != nil {
panic(err)
}
return json.RawMessage(b)
}
// isDelim simply tests to see if a json.Token is a delimeter.
func isDelim(got json.Token) bool {
switch got.(type) {
case json.Delim:
return true
}
return false
}
// delimIs tests got to see if it is want.
func delimIs(got json.Token, want rune) bool {
switch v := got.(type) {
case json.Delim:
if v == json.Delim(want) {
return true
}
}
return false
}
// hasMarshalJSON will determine if the value or a pointer to this value has
// the MarshalJSON method.
func hasMarshalJSON(v reflect.Value) bool {
if method := v.MethodByName(marshalJSON); method.Kind() != reflect.Invalid {
_, ok := v.Interface().(json.Marshaler)
return ok
}
if v.Kind() == reflect.Ptr {
v = v.Elem()
} else {
if !v.CanAddr() {
return false
}
v = v.Addr()
}
if method := v.MethodByName(marshalJSON); method.Kind() != reflect.Invalid {
_, ok := v.Interface().(json.Marshaler)
return ok
}
return false
}
// callMarshalJSON will call MarshalJSON() method on the value or a pointer to this value.
// This will panic if the method is not defined.
func callMarshalJSON(v reflect.Value) ([]byte, error) {
if method := v.MethodByName(marshalJSON); method.Kind() != reflect.Invalid {
marsh := v.Interface().(json.Marshaler)
return marsh.MarshalJSON()
}
if v.Kind() == reflect.Ptr {
v = v.Elem()
} else {
if v.CanAddr() {
v = v.Addr()
}
}
if method := v.MethodByName(unmarshalJSON); method.Kind() != reflect.Invalid {
marsh := v.Interface().(json.Marshaler)
return marsh.MarshalJSON()
}
panic(fmt.Sprintf("callMarshalJSON called on type %T that does not have MarshalJSON defined", v.Interface()))
}
// hasUnmarshalJSON will determine if the value or a pointer to this value has
// the UnmarshalJSON method.
func hasUnmarshalJSON(v reflect.Value) bool {
// You can't unmarshal on a non-pointer type.
if v.Kind() != reflect.Ptr {
if !v.CanAddr() {
return false
}
v = v.Addr()
}
if method := v.MethodByName(unmarshalJSON); method.Kind() != reflect.Invalid {
_, ok := v.Interface().(json.Unmarshaler)
return ok
}
return false
}
// hasOmitEmpty indicates if the field has instructed us to not output
// the field if omitempty is set on the tag. tag is the string
// returned by reflect.StructField.Tag().Get().
func hasOmitEmpty(tag string) bool {
sl := strings.Split(tag, ",")
for _, str := range sl {
if str == "omitempty" {
return true
}
}
return false
}

View file

@ -0,0 +1,333 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
package json
import (
"encoding/json"
"fmt"
"reflect"
)
// unmarshalMap unmarshal's a map.
func unmarshalMap(dec *json.Decoder, m reflect.Value) error {
if m.Kind() != reflect.Ptr || m.Elem().Kind() != reflect.Map {
panic("unmarshalMap called on non-*map value")
}
mapValueType := m.Elem().Type().Elem()
walk := mapWalk{dec: dec, m: m, valueType: mapValueType}
if err := walk.run(); err != nil {
return err
}
return nil
}
type mapWalk struct {
dec *json.Decoder
key string
m reflect.Value
valueType reflect.Type
}
// run runs our decoder state machine.
func (m *mapWalk) run() error {
var state = m.start
var err error
for {
state, err = state()
if err != nil {
return err
}
if state == nil {
return nil
}
}
}
func (m *mapWalk) start() (stateFn, error) {
// maps can have custom unmarshaler's.
if hasUnmarshalJSON(m.m) {
err := m.dec.Decode(m.m.Interface())
if err != nil {
return nil, err
}
return nil, nil
}
// We only want to use this if the map value is:
// *struct/struct/map/slice
// otherwise use standard decode
t, _ := m.valueBaseType()
switch t.Kind() {
case reflect.Struct, reflect.Map, reflect.Slice:
delim, err := m.dec.Token()
if err != nil {
return nil, err
}
// This indicates the value was set to JSON null.
if delim == nil {
return nil, nil
}
if !delimIs(delim, '{') {
return nil, fmt.Errorf("Unmarshal expected opening {, received %v", delim)
}
return m.next, nil
case reflect.Ptr:
return nil, fmt.Errorf("do not support maps with values of '**type' or '*reference")
}
// This is a basic map type, so just use Decode().
if err := m.dec.Decode(m.m.Interface()); err != nil {
return nil, err
}
return nil, nil
}
func (m *mapWalk) next() (stateFn, error) {
if m.dec.More() {
key, err := m.dec.Token()
if err != nil {
return nil, err
}
m.key = key.(string)
return m.storeValue, nil
}
// No more entries, so remove final }.
_, err := m.dec.Token()
if err != nil {
return nil, err
}
return nil, nil
}
func (m *mapWalk) storeValue() (stateFn, error) {
v := m.valueType
for {
switch v.Kind() {
case reflect.Ptr:
v = v.Elem()
continue
case reflect.Struct:
return m.storeStruct, nil
case reflect.Map:
return m.storeMap, nil
case reflect.Slice:
return m.storeSlice, nil
}
return nil, fmt.Errorf("bug: mapWalk.storeValue() called on unsupported type: %v", v.Kind())
}
}
func (m *mapWalk) storeStruct() (stateFn, error) {
v := newValue(m.valueType)
if err := unmarshalStruct(m.dec, v.Interface()); err != nil {
return nil, err
}
if m.valueType.Kind() == reflect.Ptr {
m.m.Elem().SetMapIndex(reflect.ValueOf(m.key), v)
return m.next, nil
}
m.m.Elem().SetMapIndex(reflect.ValueOf(m.key), v.Elem())
return m.next, nil
}
func (m *mapWalk) storeMap() (stateFn, error) {
v := reflect.MakeMap(m.valueType)
ptr := newValue(v.Type())
ptr.Elem().Set(v)
if err := unmarshalMap(m.dec, ptr); err != nil {
return nil, err
}
m.m.Elem().SetMapIndex(reflect.ValueOf(m.key), v)
return m.next, nil
}
func (m *mapWalk) storeSlice() (stateFn, error) {
v := newValue(m.valueType)
if err := unmarshalSlice(m.dec, v); err != nil {
return nil, err
}
m.m.Elem().SetMapIndex(reflect.ValueOf(m.key), v.Elem())
return m.next, nil
}
// valueType returns the underlying Type. So a *struct would yield
// struct, etc...
func (m *mapWalk) valueBaseType() (reflect.Type, bool) {
ptr := false
v := m.valueType
if v.Kind() == reflect.Ptr {
ptr = true
v = v.Elem()
}
return v, ptr
}
// unmarshalSlice unmarshal's the next value, which must be a slice, into
// ptrSlice, which must be a pointer to a slice. newValue() can be use to
// create the slice.
func unmarshalSlice(dec *json.Decoder, ptrSlice reflect.Value) error {
if ptrSlice.Kind() != reflect.Ptr || ptrSlice.Elem().Kind() != reflect.Slice {
panic("unmarshalSlice called on non-*[]slice value")
}
sliceValueType := ptrSlice.Elem().Type().Elem()
walk := sliceWalk{
dec: dec,
s: ptrSlice,
valueType: sliceValueType,
}
if err := walk.run(); err != nil {
return err
}
return nil
}
type sliceWalk struct {
dec *json.Decoder
s reflect.Value // *[]slice
valueType reflect.Type
}
// run runs our decoder state machine.
func (s *sliceWalk) run() error {
var state = s.start
var err error
for {
state, err = state()
if err != nil {
return err
}
if state == nil {
return nil
}
}
}
func (s *sliceWalk) start() (stateFn, error) {
// slices can have custom unmarshaler's.
if hasUnmarshalJSON(s.s) {
err := s.dec.Decode(s.s.Interface())
if err != nil {
return nil, err
}
return nil, nil
}
// We only want to use this if the slice value is:
// []*struct/[]struct/[]map/[]slice
// otherwise use standard decode
t := s.valueBaseType()
switch t.Kind() {
case reflect.Ptr:
return nil, fmt.Errorf("cannot unmarshal into a **<type> or *<reference>")
case reflect.Struct, reflect.Map, reflect.Slice:
delim, err := s.dec.Token()
if err != nil {
return nil, err
}
// This indicates the value was set to nil.
if delim == nil {
return nil, nil
}
if !delimIs(delim, '[') {
return nil, fmt.Errorf("Unmarshal expected opening [, received %v", delim)
}
return s.next, nil
}
if err := s.dec.Decode(s.s.Interface()); err != nil {
return nil, err
}
return nil, nil
}
func (s *sliceWalk) next() (stateFn, error) {
if s.dec.More() {
return s.storeValue, nil
}
// Nothing left in the slice, remove closing ]
_, err := s.dec.Token()
return nil, err
}
func (s *sliceWalk) storeValue() (stateFn, error) {
t := s.valueBaseType()
switch t.Kind() {
case reflect.Ptr:
return nil, fmt.Errorf("do not support 'pointer to pointer' or 'pointer to reference' types")
case reflect.Struct:
return s.storeStruct, nil
case reflect.Map:
return s.storeMap, nil
case reflect.Slice:
return s.storeSlice, nil
}
return nil, fmt.Errorf("bug: sliceWalk.storeValue() called on unsupported type: %v", t.Kind())
}
func (s *sliceWalk) storeStruct() (stateFn, error) {
v := newValue(s.valueType)
if err := unmarshalStruct(s.dec, v.Interface()); err != nil {
return nil, err
}
if s.valueType.Kind() == reflect.Ptr {
s.s.Elem().Set(reflect.Append(s.s.Elem(), v))
return s.next, nil
}
s.s.Elem().Set(reflect.Append(s.s.Elem(), v.Elem()))
return s.next, nil
}
func (s *sliceWalk) storeMap() (stateFn, error) {
v := reflect.MakeMap(s.valueType)
ptr := newValue(v.Type())
ptr.Elem().Set(v)
if err := unmarshalMap(s.dec, ptr); err != nil {
return nil, err
}
s.s.Elem().Set(reflect.Append(s.s.Elem(), v))
return s.next, nil
}
func (s *sliceWalk) storeSlice() (stateFn, error) {
v := newValue(s.valueType)
if err := unmarshalSlice(s.dec, v); err != nil {
return nil, err
}
s.s.Elem().Set(reflect.Append(s.s.Elem(), v.Elem()))
return s.next, nil
}
// valueType returns the underlying Type. So a *struct would yield
// struct, etc...
func (s *sliceWalk) valueBaseType() reflect.Type {
v := s.valueType
if v.Kind() == reflect.Ptr {
v = v.Elem()
}
return v
}
// newValue() returns a new *type that represents type passed.
func newValue(valueType reflect.Type) reflect.Value {
if valueType.Kind() == reflect.Ptr {
return reflect.New(valueType.Elem())
}
return reflect.New(valueType)
}

View file

@ -0,0 +1,346 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
package json
import (
"bytes"
"encoding/json"
"fmt"
"reflect"
"unicode"
)
// marshalStruct takes in i, which must be a *struct or struct and marshals its content
// as JSON into buff (sometimes with writes to buff directly, sometimes via enc).
// This call is recursive for all fields of *struct or struct type.
func marshalStruct(v reflect.Value, buff *bytes.Buffer, enc *json.Encoder) error {
if v.Kind() == reflect.Ptr {
v = v.Elem()
}
// We only care about custom Marshalling a struct.
if v.Kind() != reflect.Struct {
return fmt.Errorf("bug: marshal() received a non *struct or struct, received type %T", v.Interface())
}
if hasMarshalJSON(v) {
b, err := callMarshalJSON(v)
if err != nil {
return err
}
buff.Write(b)
return nil
}
t := v.Type()
// If it has an AdditionalFields field make sure its the right type.
f := v.FieldByName(addField)
if f.Kind() != reflect.Invalid {
if f.Kind() != reflect.Map {
return fmt.Errorf("type %T has field 'AdditionalFields' that is not a map[string]interface{}", v.Interface())
}
if !f.Type().AssignableTo(mapStrInterType) {
return fmt.Errorf("type %T has field 'AdditionalFields' that is not a map[string]interface{}", v.Interface())
}
}
translator, err := findFields(v)
if err != nil {
return err
}
buff.WriteByte(leftBrace)
for x := 0; x < v.NumField(); x++ {
field := v.Field(x)
// We don't access private fields.
if unicode.IsLower(rune(t.Field(x).Name[0])) {
continue
}
if t.Field(x).Name == addField {
if v.Field(x).Len() > 0 {
if err := writeAddFields(field.Interface(), buff, enc); err != nil {
return err
}
buff.WriteByte(comma)
}
continue
}
// If they have omitempty set, we don't write out the field if
// it is the zero value.
if hasOmitEmpty(t.Field(x).Tag.Get("json")) {
if v.Field(x).IsZero() {
continue
}
}
// Write out the field name part.
jsonName := translator.jsonName(t.Field(x).Name)
buff.WriteString(fmt.Sprintf("%q:", jsonName))
if field.Kind() == reflect.Ptr {
field = field.Elem()
}
if err := marshalStructField(field, buff, enc); err != nil {
return err
}
}
buff.Truncate(buff.Len() - 1) // Remove final comma
buff.WriteByte(rightBrace)
return nil
}
func marshalStructField(field reflect.Value, buff *bytes.Buffer, enc *json.Encoder) error {
// Determine if we need a trailing comma.
defer buff.WriteByte(comma)
switch field.Kind() {
// If it was a *struct or struct, we need to recursively all marshal().
case reflect.Struct:
if field.CanAddr() {
field = field.Addr()
}
return marshalStruct(field, buff, enc)
case reflect.Map:
return marshalMap(field, buff, enc)
case reflect.Slice:
return marshalSlice(field, buff, enc)
}
// It is just a basic type, so encode it.
if err := enc.Encode(field.Interface()); err != nil {
return err
}
buff.Truncate(buff.Len() - 1) // Remove Encode() added \n
return nil
}
func marshalMap(v reflect.Value, buff *bytes.Buffer, enc *json.Encoder) error {
if v.Kind() != reflect.Map {
return fmt.Errorf("bug: marshalMap() called on %T", v.Interface())
}
if v.Len() == 0 {
buff.WriteByte(leftBrace)
buff.WriteByte(rightBrace)
return nil
}
encoder := mapEncode{m: v, buff: buff, enc: enc}
return encoder.run()
}
type mapEncode struct {
m reflect.Value
buff *bytes.Buffer
enc *json.Encoder
valueBaseType reflect.Type
}
// run runs our encoder state machine.
func (m *mapEncode) run() error {
var state = m.start
var err error
for {
state, err = state()
if err != nil {
return err
}
if state == nil {
return nil
}
}
}
func (m *mapEncode) start() (stateFn, error) {
if hasMarshalJSON(m.m) {
b, err := callMarshalJSON(m.m)
if err != nil {
return nil, err
}
m.buff.Write(b)
return nil, nil
}
valueBaseType := m.m.Type().Elem()
if valueBaseType.Kind() == reflect.Ptr {
valueBaseType = valueBaseType.Elem()
}
m.valueBaseType = valueBaseType
switch valueBaseType.Kind() {
case reflect.Ptr:
return nil, fmt.Errorf("Marshal does not support **<type> or *<reference>")
case reflect.Struct, reflect.Map, reflect.Slice:
return m.encode, nil
}
// If the map value doesn't have a struct/map/slice, just Encode() it.
if err := m.enc.Encode(m.m.Interface()); err != nil {
return nil, err
}
m.buff.Truncate(m.buff.Len() - 1) // Remove Encode() added \n
return nil, nil
}
func (m *mapEncode) encode() (stateFn, error) {
m.buff.WriteByte(leftBrace)
iter := m.m.MapRange()
for iter.Next() {
// Write the key.
k := iter.Key()
m.buff.WriteString(fmt.Sprintf("%q:", k.String()))
v := iter.Value()
switch m.valueBaseType.Kind() {
case reflect.Struct:
if v.CanAddr() {
v = v.Addr()
}
if err := marshalStruct(v, m.buff, m.enc); err != nil {
return nil, err
}
case reflect.Map:
if err := marshalMap(v, m.buff, m.enc); err != nil {
return nil, err
}
case reflect.Slice:
if err := marshalSlice(v, m.buff, m.enc); err != nil {
return nil, err
}
default:
panic(fmt.Sprintf("critical bug: mapEncode.encode() called with value base type: %v", m.valueBaseType.Kind()))
}
m.buff.WriteByte(comma)
}
m.buff.Truncate(m.buff.Len() - 1) // Remove final comma
m.buff.WriteByte(rightBrace)
return nil, nil
}
func marshalSlice(v reflect.Value, buff *bytes.Buffer, enc *json.Encoder) error {
if v.Kind() != reflect.Slice {
return fmt.Errorf("bug: marshalSlice() called on %T", v.Interface())
}
if v.Len() == 0 {
buff.WriteByte(leftParen)
buff.WriteByte(rightParen)
return nil
}
encoder := sliceEncode{s: v, buff: buff, enc: enc}
return encoder.run()
}
type sliceEncode struct {
s reflect.Value
buff *bytes.Buffer
enc *json.Encoder
valueBaseType reflect.Type
}
// run runs our encoder state machine.
func (s *sliceEncode) run() error {
var state = s.start
var err error
for {
state, err = state()
if err != nil {
return err
}
if state == nil {
return nil
}
}
}
func (s *sliceEncode) start() (stateFn, error) {
if hasMarshalJSON(s.s) {
b, err := callMarshalJSON(s.s)
if err != nil {
return nil, err
}
s.buff.Write(b)
return nil, nil
}
valueBaseType := s.s.Type().Elem()
if valueBaseType.Kind() == reflect.Ptr {
valueBaseType = valueBaseType.Elem()
}
s.valueBaseType = valueBaseType
switch valueBaseType.Kind() {
case reflect.Ptr:
return nil, fmt.Errorf("Marshal does not support **<type> or *<reference>")
case reflect.Struct, reflect.Map, reflect.Slice:
return s.encode, nil
}
// If the map value doesn't have a struct/map/slice, just Encode() it.
if err := s.enc.Encode(s.s.Interface()); err != nil {
return nil, err
}
s.buff.Truncate(s.buff.Len() - 1) // Remove Encode added \n
return nil, nil
}
func (s *sliceEncode) encode() (stateFn, error) {
s.buff.WriteByte(leftParen)
for i := 0; i < s.s.Len(); i++ {
v := s.s.Index(i)
switch s.valueBaseType.Kind() {
case reflect.Struct:
if v.CanAddr() {
v = v.Addr()
}
if err := marshalStruct(v, s.buff, s.enc); err != nil {
return nil, err
}
case reflect.Map:
if err := marshalMap(v, s.buff, s.enc); err != nil {
return nil, err
}
case reflect.Slice:
if err := marshalSlice(v, s.buff, s.enc); err != nil {
return nil, err
}
default:
panic(fmt.Sprintf("critical bug: mapEncode.encode() called with value base type: %v", s.valueBaseType.Kind()))
}
s.buff.WriteByte(comma)
}
s.buff.Truncate(s.buff.Len() - 1) // Remove final comma
s.buff.WriteByte(rightParen)
return nil, nil
}
// writeAddFields writes the AdditionalFields struct field out to JSON as field
// values. i must be a map[string]interface{} or this will panic.
func writeAddFields(i interface{}, buff *bytes.Buffer, enc *json.Encoder) error {
m := i.(map[string]interface{})
x := 0
for k, v := range m {
buff.WriteString(fmt.Sprintf("%q:", k))
if err := enc.Encode(v); err != nil {
return err
}
buff.Truncate(buff.Len() - 1) // Remove Encode() added \n
if x+1 != len(m) {
buff.WriteByte(comma)
}
x++
}
return nil
}

View file

@ -0,0 +1,290 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
package json
import (
"encoding/json"
"fmt"
"reflect"
"strings"
)
func unmarshalStruct(jdec *json.Decoder, i interface{}) error {
v := reflect.ValueOf(i)
if v.Kind() != reflect.Ptr {
return fmt.Errorf("Unmarshal() received type %T, which is not a *struct", i)
}
v = v.Elem()
if v.Kind() != reflect.Struct {
return fmt.Errorf("Unmarshal() received type %T, which is not a *struct", i)
}
if hasUnmarshalJSON(v) {
// Indicates that this type has a custom Unmarshaler.
return jdec.Decode(v.Addr().Interface())
}
f := v.FieldByName(addField)
if f.Kind() == reflect.Invalid {
return fmt.Errorf("Unmarshal(%T) only supports structs that have the field AdditionalFields or implements json.Unmarshaler", i)
}
if f.Kind() != reflect.Map || !f.Type().AssignableTo(mapStrInterType) {
return fmt.Errorf("type %T has field 'AdditionalFields' that is not a map[string]interface{}", i)
}
dec := newDecoder(jdec, v)
return dec.run()
}
type decoder struct {
dec *json.Decoder
value reflect.Value // This will be a reflect.Struct
translator translateFields
key string
}
func newDecoder(dec *json.Decoder, value reflect.Value) *decoder {
return &decoder{value: value, dec: dec}
}
// run runs our decoder state machine.
func (d *decoder) run() error {
var state = d.start
var err error
for {
state, err = state()
if err != nil {
return err
}
if state == nil {
return nil
}
}
}
// start looks for our opening delimeter '{' and then transitions to looping through our fields.
func (d *decoder) start() (stateFn, error) {
var err error
d.translator, err = findFields(d.value)
if err != nil {
return nil, err
}
delim, err := d.dec.Token()
if err != nil {
return nil, err
}
if !delimIs(delim, '{') {
return nil, fmt.Errorf("Unmarshal expected opening {, received %v", delim)
}
return d.next, nil
}
// next gets the next struct field name from the raw json or stops the machine if we get our closing }.
func (d *decoder) next() (stateFn, error) {
if !d.dec.More() {
// Remove the closing }.
if _, err := d.dec.Token(); err != nil {
return nil, err
}
return nil, nil
}
key, err := d.dec.Token()
if err != nil {
return nil, err
}
d.key = key.(string)
return d.storeValue, nil
}
// storeValue takes the next value and stores it our struct. If the field can't be found
// in the struct, it pushes the operation to storeAdditional().
func (d *decoder) storeValue() (stateFn, error) {
goName := d.translator.goName(d.key)
if goName == "" {
goName = d.key
}
// We don't have the field in the struct, so it goes in AdditionalFields.
f := d.value.FieldByName(goName)
if f.Kind() == reflect.Invalid {
return d.storeAdditional, nil
}
// Indicates that this type has a custom Unmarshaler.
if hasUnmarshalJSON(f) {
err := d.dec.Decode(f.Addr().Interface())
if err != nil {
return nil, err
}
return d.next, nil
}
t, isPtr, err := fieldBaseType(d.value, goName)
if err != nil {
return nil, fmt.Errorf("type(%s) had field(%s) %w", d.value.Type().Name(), goName, err)
}
switch t.Kind() {
// We need to recursively call ourselves on any *struct or struct.
case reflect.Struct:
if isPtr {
if f.IsNil() {
f.Set(reflect.New(t))
}
} else {
f = f.Addr()
}
if err := unmarshalStruct(d.dec, f.Interface()); err != nil {
return nil, err
}
return d.next, nil
case reflect.Map:
v := reflect.MakeMap(f.Type())
ptr := newValue(f.Type())
ptr.Elem().Set(v)
if err := unmarshalMap(d.dec, ptr); err != nil {
return nil, err
}
f.Set(ptr.Elem())
return d.next, nil
case reflect.Slice:
v := reflect.MakeSlice(f.Type(), 0, 0)
ptr := newValue(f.Type())
ptr.Elem().Set(v)
if err := unmarshalSlice(d.dec, ptr); err != nil {
return nil, err
}
f.Set(ptr.Elem())
return d.next, nil
}
if !isPtr {
f = f.Addr()
}
// For values that are pointers, we need them to be non-nil in order
// to decode into them.
if f.IsNil() {
f.Set(reflect.New(t))
}
if err := d.dec.Decode(f.Interface()); err != nil {
return nil, err
}
return d.next, nil
}
// storeAdditional pushes the key/value into our .AdditionalFields map.
func (d *decoder) storeAdditional() (stateFn, error) {
rw := json.RawMessage{}
if err := d.dec.Decode(&rw); err != nil {
return nil, err
}
field := d.value.FieldByName(addField)
if field.IsNil() {
field.Set(reflect.MakeMap(field.Type()))
}
field.SetMapIndex(reflect.ValueOf(d.key), reflect.ValueOf(rw))
return d.next, nil
}
func fieldBaseType(v reflect.Value, fieldName string) (t reflect.Type, isPtr bool, err error) {
sf, ok := v.Type().FieldByName(fieldName)
if !ok {
return nil, false, fmt.Errorf("bug: fieldBaseType() lookup of field(%s) on type(%s): do not have field", fieldName, v.Type().Name())
}
t = sf.Type
if t.Kind() == reflect.Ptr {
t = t.Elem()
isPtr = true
}
if t.Kind() == reflect.Ptr {
return nil, isPtr, fmt.Errorf("received pointer to pointer type, not supported")
}
return t, isPtr, nil
}
type translateField struct {
jsonName string
goName string
}
// translateFields is a list of translateFields with a handy lookup method.
type translateFields []translateField
// goName loops through a list of fields looking for one contaning the jsonName and
// returning the goName. If not found, returns the empty string.
// Note: not a map because at this size slices are faster even in tight loops.
func (t translateFields) goName(jsonName string) string {
for _, entry := range t {
if entry.jsonName == jsonName {
return entry.goName
}
}
return ""
}
// jsonName loops through a list of fields looking for one contaning the goName and
// returning the jsonName. If not found, returns the empty string.
// Note: not a map because at this size slices are faster even in tight loops.
func (t translateFields) jsonName(goName string) string {
for _, entry := range t {
if entry.goName == goName {
return entry.jsonName
}
}
return ""
}
var umarshalerType = reflect.TypeOf((*json.Unmarshaler)(nil)).Elem()
// findFields parses a struct and writes the field tags for lookup. It will return an error
// if any field has a type of *struct or struct that does not implement json.Marshaler.
func findFields(v reflect.Value) (translateFields, error) {
if v.Kind() == reflect.Ptr {
v = v.Elem()
}
if v.Kind() != reflect.Struct {
return nil, fmt.Errorf("findFields received a %s type, expected *struct or struct", v.Type().Name())
}
tfs := make([]translateField, 0, v.NumField())
for i := 0; i < v.NumField(); i++ {
tf := translateField{
goName: v.Type().Field(i).Name,
jsonName: parseTag(v.Type().Field(i).Tag.Get("json")),
}
switch tf.jsonName {
case "", "-":
tf.jsonName = tf.goName
}
tfs = append(tfs, tf)
f := v.Field(i)
if f.Kind() == reflect.Ptr {
f = f.Elem()
}
if f.Kind() == reflect.Struct {
if f.Type().Implements(umarshalerType) {
return nil, fmt.Errorf("struct type %q which has field %q which "+
"doesn't implement json.Unmarshaler", v.Type().Name(), v.Type().Field(i).Name)
}
}
}
return tfs, nil
}
// parseTag just returns the first entry in the tag. tag is the string
// returned by reflect.StructField.Tag().Get().
func parseTag(tag string) string {
if idx := strings.Index(tag, ","); idx != -1 {
return tag[:idx]
}
return tag
}

View file

@ -0,0 +1,70 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
// Package time provides for custom types to translate time from JSON and other formats
// into time.Time objects.
package time
import (
"fmt"
"strconv"
"strings"
"time"
)
// Unix provides a type that can marshal and unmarshal a string representation
// of the unix epoch into a time.Time object.
type Unix struct {
T time.Time
}
// MarshalJSON implements encoding/json.MarshalJSON().
func (u Unix) MarshalJSON() ([]byte, error) {
if u.T.IsZero() {
return []byte(""), nil
}
return []byte(fmt.Sprintf("%q", strconv.FormatInt(u.T.Unix(), 10))), nil
}
// UnmarshalJSON implements encoding/json.UnmarshalJSON().
func (u *Unix) UnmarshalJSON(b []byte) error {
i, err := strconv.Atoi(strings.Trim(string(b), `"`))
if err != nil {
return fmt.Errorf("unix time(%s) could not be converted from string to int: %w", string(b), err)
}
u.T = time.Unix(int64(i), 0)
return nil
}
// DurationTime provides a type that can marshal and unmarshal a string representation
// of a duration from now into a time.Time object.
// Note: I'm not sure this is the best way to do this. What happens is we get a field
// called "expires_in" that represents the seconds from now that this expires. We
// turn that into a time we call .ExpiresOn. But maybe we should be recording
// when the token was received at .TokenRecieved and .ExpiresIn should remain as a duration.
// Then we could have a method called ExpiresOn(). Honestly, the whole thing is
// bad because the server doesn't return a concrete time. I think this is
// cleaner, but its not great either.
type DurationTime struct {
T time.Time
}
// MarshalJSON implements encoding/json.MarshalJSON().
func (d DurationTime) MarshalJSON() ([]byte, error) {
if d.T.IsZero() {
return []byte(""), nil
}
dt := time.Until(d.T)
return []byte(fmt.Sprintf("%d", int64(dt*time.Second))), nil
}
// UnmarshalJSON implements encoding/json.UnmarshalJSON().
func (d *DurationTime) UnmarshalJSON(b []byte) error {
i, err := strconv.Atoi(strings.Trim(string(b), `"`))
if err != nil {
return fmt.Errorf("unix time(%s) could not be converted from string to int: %w", string(b), err)
}
d.T = time.Now().Add(time.Duration(i) * time.Second)
return nil
}

View file

@ -0,0 +1,177 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
// Package local contains a local HTTP server used with interactive authentication.
package local
import (
"context"
"fmt"
"net"
"net/http"
"strconv"
"strings"
"time"
)
var okPage = []byte(`
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Authentication Complete</title>
</head>
<body>
<p>Authentication complete. You can return to the application. Feel free to close this browser tab.</p>
</body>
</html>
`)
const failPage = `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Authentication Failed</title>
</head>
<body>
<p>Authentication failed. You can return to the application. Feel free to close this browser tab.</p>
<p>Error details: error %s error_description: %s</p>
</body>
</html>
`
// Result is the result from the redirect.
type Result struct {
// Code is the code sent by the authority server.
Code string
// Err is set if there was an error.
Err error
}
// Server is an HTTP server.
type Server struct {
// Addr is the address the server is listening on.
Addr string
resultCh chan Result
s *http.Server
reqState string
}
// New creates a local HTTP server and starts it.
func New(reqState string, port int) (*Server, error) {
var l net.Listener
var err error
var portStr string
if port > 0 {
// use port provided by caller
l, err = net.Listen("tcp", fmt.Sprintf("localhost:%d", port))
portStr = strconv.FormatInt(int64(port), 10)
} else {
// find a free port
for i := 0; i < 10; i++ {
l, err = net.Listen("tcp", "localhost:0")
if err != nil {
continue
}
addr := l.Addr().String()
portStr = addr[strings.LastIndex(addr, ":")+1:]
break
}
}
if err != nil {
return nil, err
}
serv := &Server{
Addr: fmt.Sprintf("http://localhost:%s", portStr),
s: &http.Server{Addr: "localhost:0", ReadHeaderTimeout: time.Second},
reqState: reqState,
resultCh: make(chan Result, 1),
}
serv.s.Handler = http.HandlerFunc(serv.handler)
if err := serv.start(l); err != nil {
return nil, err
}
return serv, nil
}
func (s *Server) start(l net.Listener) error {
go func() {
err := s.s.Serve(l)
if err != nil {
select {
case s.resultCh <- Result{Err: err}:
default:
}
}
}()
return nil
}
// Result gets the result of the redirect operation. Once a single result is returned, the server
// is shutdown. ctx deadline will be honored.
func (s *Server) Result(ctx context.Context) Result {
select {
case <-ctx.Done():
return Result{Err: ctx.Err()}
case r := <-s.resultCh:
return r
}
}
// Shutdown shuts down the server.
func (s *Server) Shutdown() {
// Note: You might get clever and think you can do this in handler() as a defer, you can't.
_ = s.s.Shutdown(context.Background())
}
func (s *Server) putResult(r Result) {
select {
case s.resultCh <- r:
default:
}
}
func (s *Server) handler(w http.ResponseWriter, r *http.Request) {
q := r.URL.Query()
headerErr := q.Get("error")
if headerErr != "" {
desc := q.Get("error_description")
// Note: It is a little weird we handle some errors by not going to the failPage. If they all should,
// change this to s.error() and make s.error() write the failPage instead of an error code.
_, _ = w.Write([]byte(fmt.Sprintf(failPage, headerErr, desc)))
s.putResult(Result{Err: fmt.Errorf(desc)})
return
}
respState := q.Get("state")
switch respState {
case s.reqState:
case "":
s.error(w, http.StatusInternalServerError, "server didn't send OAuth state")
return
default:
s.error(w, http.StatusInternalServerError, "mismatched OAuth state, req(%s), resp(%s)", s.reqState, respState)
return
}
code := q.Get("code")
if code == "" {
s.error(w, http.StatusInternalServerError, "authorization code missing in query string")
return
}
_, _ = w.Write(okPage)
s.putResult(Result{Code: code})
}
func (s *Server) error(w http.ResponseWriter, code int, str string, i ...interface{}) {
err := fmt.Errorf(str, i...)
http.Error(w, err.Error(), code)
s.putResult(Result{Err: err})
}

View file

@ -0,0 +1,353 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
package oauth
import (
"context"
"encoding/json"
"fmt"
"io"
"time"
"github.com/AzureAD/microsoft-authentication-library-for-go/apps/errors"
"github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/exported"
internalTime "github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/json/types/time"
"github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/oauth/ops"
"github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/oauth/ops/accesstokens"
"github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/oauth/ops/authority"
"github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/oauth/ops/wstrust"
"github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/oauth/ops/wstrust/defs"
"github.com/google/uuid"
)
// ResolveEndpointer contains the methods for resolving authority endpoints.
type ResolveEndpointer interface {
ResolveEndpoints(ctx context.Context, authorityInfo authority.Info, userPrincipalName string) (authority.Endpoints, error)
}
// AccessTokens contains the methods for fetching tokens from different sources.
type AccessTokens interface {
DeviceCodeResult(ctx context.Context, authParameters authority.AuthParams) (accesstokens.DeviceCodeResult, error)
FromUsernamePassword(ctx context.Context, authParameters authority.AuthParams) (accesstokens.TokenResponse, error)
FromAuthCode(ctx context.Context, req accesstokens.AuthCodeRequest) (accesstokens.TokenResponse, error)
FromRefreshToken(ctx context.Context, appType accesstokens.AppType, authParams authority.AuthParams, cc *accesstokens.Credential, refreshToken string) (accesstokens.TokenResponse, error)
FromClientSecret(ctx context.Context, authParameters authority.AuthParams, clientSecret string) (accesstokens.TokenResponse, error)
FromAssertion(ctx context.Context, authParameters authority.AuthParams, assertion string) (accesstokens.TokenResponse, error)
FromUserAssertionClientSecret(ctx context.Context, authParameters authority.AuthParams, userAssertion string, clientSecret string) (accesstokens.TokenResponse, error)
FromUserAssertionClientCertificate(ctx context.Context, authParameters authority.AuthParams, userAssertion string, assertion string) (accesstokens.TokenResponse, error)
FromDeviceCodeResult(ctx context.Context, authParameters authority.AuthParams, deviceCodeResult accesstokens.DeviceCodeResult) (accesstokens.TokenResponse, error)
FromSamlGrant(ctx context.Context, authParameters authority.AuthParams, samlGrant wstrust.SamlTokenInfo) (accesstokens.TokenResponse, error)
}
// FetchAuthority will be implemented by authority.Authority.
type FetchAuthority interface {
UserRealm(context.Context, authority.AuthParams) (authority.UserRealm, error)
AADInstanceDiscovery(context.Context, authority.Info) (authority.InstanceDiscoveryResponse, error)
}
// FetchWSTrust contains the methods for interacting with WSTrust endpoints.
type FetchWSTrust interface {
Mex(ctx context.Context, federationMetadataURL string) (defs.MexDocument, error)
SAMLTokenInfo(ctx context.Context, authParameters authority.AuthParams, cloudAudienceURN string, endpoint defs.Endpoint) (wstrust.SamlTokenInfo, error)
}
// Client provides tokens for various types of token requests.
type Client struct {
Resolver ResolveEndpointer
AccessTokens AccessTokens
Authority FetchAuthority
WSTrust FetchWSTrust
}
// New is the constructor for Token.
func New(httpClient ops.HTTPClient) *Client {
r := ops.New(httpClient)
return &Client{
Resolver: newAuthorityEndpoint(r),
AccessTokens: r.AccessTokens(),
Authority: r.Authority(),
WSTrust: r.WSTrust(),
}
}
// ResolveEndpoints gets the authorization and token endpoints and creates an AuthorityEndpoints instance.
func (t *Client) ResolveEndpoints(ctx context.Context, authorityInfo authority.Info, userPrincipalName string) (authority.Endpoints, error) {
return t.Resolver.ResolveEndpoints(ctx, authorityInfo, userPrincipalName)
}
// AADInstanceDiscovery attempts to discover a tenant endpoint (used in OIDC auth with an authorization endpoint).
// This is done by AAD which allows for aliasing of tenants (windows.sts.net is the same as login.windows.com).
func (t *Client) AADInstanceDiscovery(ctx context.Context, authorityInfo authority.Info) (authority.InstanceDiscoveryResponse, error) {
return t.Authority.AADInstanceDiscovery(ctx, authorityInfo)
}
// AuthCode returns a token based on an authorization code.
func (t *Client) AuthCode(ctx context.Context, req accesstokens.AuthCodeRequest) (accesstokens.TokenResponse, error) {
if err := scopeError(req.AuthParams); err != nil {
return accesstokens.TokenResponse{}, err
}
if err := t.resolveEndpoint(ctx, &req.AuthParams, ""); err != nil {
return accesstokens.TokenResponse{}, err
}
tResp, err := t.AccessTokens.FromAuthCode(ctx, req)
if err != nil {
return accesstokens.TokenResponse{}, fmt.Errorf("could not retrieve token from auth code: %w", err)
}
return tResp, nil
}
// Credential acquires a token from the authority using a client credentials grant.
func (t *Client) Credential(ctx context.Context, authParams authority.AuthParams, cred *accesstokens.Credential) (accesstokens.TokenResponse, error) {
if cred.TokenProvider != nil {
now := time.Now()
scopes := make([]string, len(authParams.Scopes))
copy(scopes, authParams.Scopes)
params := exported.TokenProviderParameters{
Claims: authParams.Claims,
CorrelationID: uuid.New().String(),
Scopes: scopes,
TenantID: authParams.AuthorityInfo.Tenant,
}
tr, err := cred.TokenProvider(ctx, params)
if err != nil {
if len(scopes) == 0 {
err = fmt.Errorf("token request had an empty authority.AuthParams.Scopes, which may cause the following error: %w", err)
return accesstokens.TokenResponse{}, err
}
return accesstokens.TokenResponse{}, err
}
return accesstokens.TokenResponse{
AccessToken: tr.AccessToken,
ExpiresOn: internalTime.DurationTime{
T: now.Add(time.Duration(tr.ExpiresInSeconds) * time.Second),
},
GrantedScopes: accesstokens.Scopes{Slice: authParams.Scopes},
}, nil
}
if err := t.resolveEndpoint(ctx, &authParams, ""); err != nil {
return accesstokens.TokenResponse{}, err
}
if cred.Secret != "" {
return t.AccessTokens.FromClientSecret(ctx, authParams, cred.Secret)
}
jwt, err := cred.JWT(ctx, authParams)
if err != nil {
return accesstokens.TokenResponse{}, err
}
return t.AccessTokens.FromAssertion(ctx, authParams, jwt)
}
// Credential acquires a token from the authority using a client credentials grant.
func (t *Client) OnBehalfOf(ctx context.Context, authParams authority.AuthParams, cred *accesstokens.Credential) (accesstokens.TokenResponse, error) {
if err := scopeError(authParams); err != nil {
return accesstokens.TokenResponse{}, err
}
if err := t.resolveEndpoint(ctx, &authParams, ""); err != nil {
return accesstokens.TokenResponse{}, err
}
if cred.Secret != "" {
return t.AccessTokens.FromUserAssertionClientSecret(ctx, authParams, authParams.UserAssertion, cred.Secret)
}
jwt, err := cred.JWT(ctx, authParams)
if err != nil {
return accesstokens.TokenResponse{}, err
}
tr, err := t.AccessTokens.FromUserAssertionClientCertificate(ctx, authParams, authParams.UserAssertion, jwt)
if err != nil {
return accesstokens.TokenResponse{}, err
}
return tr, nil
}
func (t *Client) Refresh(ctx context.Context, reqType accesstokens.AppType, authParams authority.AuthParams, cc *accesstokens.Credential, refreshToken accesstokens.RefreshToken) (accesstokens.TokenResponse, error) {
if err := scopeError(authParams); err != nil {
return accesstokens.TokenResponse{}, err
}
if err := t.resolveEndpoint(ctx, &authParams, ""); err != nil {
return accesstokens.TokenResponse{}, err
}
tr, err := t.AccessTokens.FromRefreshToken(ctx, reqType, authParams, cc, refreshToken.Secret)
if err != nil {
return accesstokens.TokenResponse{}, err
}
return tr, nil
}
// UsernamePassword retrieves a token where a username and password is used. However, if this is
// a user realm of "Federated", this uses SAML tokens. If "Managed", uses normal username/password.
func (t *Client) UsernamePassword(ctx context.Context, authParams authority.AuthParams) (accesstokens.TokenResponse, error) {
if err := scopeError(authParams); err != nil {
return accesstokens.TokenResponse{}, err
}
if authParams.AuthorityInfo.AuthorityType == authority.ADFS {
if err := t.resolveEndpoint(ctx, &authParams, authParams.Username); err != nil {
return accesstokens.TokenResponse{}, err
}
return t.AccessTokens.FromUsernamePassword(ctx, authParams)
}
if err := t.resolveEndpoint(ctx, &authParams, ""); err != nil {
return accesstokens.TokenResponse{}, err
}
userRealm, err := t.Authority.UserRealm(ctx, authParams)
if err != nil {
return accesstokens.TokenResponse{}, fmt.Errorf("problem getting user realm from authority: %w", err)
}
switch userRealm.AccountType {
case authority.Federated:
mexDoc, err := t.WSTrust.Mex(ctx, userRealm.FederationMetadataURL)
if err != nil {
err = fmt.Errorf("problem getting mex doc from federated url(%s): %w", userRealm.FederationMetadataURL, err)
return accesstokens.TokenResponse{}, err
}
saml, err := t.WSTrust.SAMLTokenInfo(ctx, authParams, userRealm.CloudAudienceURN, mexDoc.UsernamePasswordEndpoint)
if err != nil {
err = fmt.Errorf("problem getting SAML token info: %w", err)
return accesstokens.TokenResponse{}, err
}
tr, err := t.AccessTokens.FromSamlGrant(ctx, authParams, saml)
if err != nil {
return accesstokens.TokenResponse{}, err
}
return tr, nil
case authority.Managed:
if len(authParams.Scopes) == 0 {
err = fmt.Errorf("token request had an empty authority.AuthParams.Scopes, which may cause the following error: %w", err)
return accesstokens.TokenResponse{}, err
}
return t.AccessTokens.FromUsernamePassword(ctx, authParams)
}
return accesstokens.TokenResponse{}, errors.New("unknown account type")
}
// DeviceCode is the result of a call to Token.DeviceCode().
type DeviceCode struct {
// Result is the device code result from the first call in the device code flow. This allows
// the caller to retrieve the displayed code that is used to authorize on the second device.
Result accesstokens.DeviceCodeResult
authParams authority.AuthParams
accessTokens AccessTokens
}
// Token returns a token AFTER the user uses the user code on the second device. This will block
// until either: (1) the code is input by the user and the service releases a token, (2) the token
// expires, (3) the Context passed to .DeviceCode() is cancelled or expires, (4) some other service
// error occurs.
func (d DeviceCode) Token(ctx context.Context) (accesstokens.TokenResponse, error) {
if d.accessTokens == nil {
return accesstokens.TokenResponse{}, fmt.Errorf("DeviceCode was either created outside its package or the creating method had an error. DeviceCode is not valid")
}
var cancel context.CancelFunc
if deadline, ok := ctx.Deadline(); !ok || d.Result.ExpiresOn.Before(deadline) {
ctx, cancel = context.WithDeadline(ctx, d.Result.ExpiresOn)
} else {
ctx, cancel = context.WithCancel(ctx)
}
defer cancel()
var interval = 50 * time.Millisecond
timer := time.NewTimer(interval)
defer timer.Stop()
for {
timer.Reset(interval)
select {
case <-ctx.Done():
return accesstokens.TokenResponse{}, ctx.Err()
case <-timer.C:
interval += interval * 2
if interval > 5*time.Second {
interval = 5 * time.Second
}
}
token, err := d.accessTokens.FromDeviceCodeResult(ctx, d.authParams, d.Result)
if err != nil && isWaitDeviceCodeErr(err) {
continue
}
return token, err // This handles if it was a non-wait error or success
}
}
type deviceCodeError struct {
Error string `json:"error"`
}
func isWaitDeviceCodeErr(err error) bool {
var c errors.CallErr
if !errors.As(err, &c) {
return false
}
if c.Resp.StatusCode != 400 {
return false
}
var dCErr deviceCodeError
defer c.Resp.Body.Close()
body, err := io.ReadAll(c.Resp.Body)
if err != nil {
return false
}
err = json.Unmarshal(body, &dCErr)
if err != nil {
return false
}
if dCErr.Error == "authorization_pending" || dCErr.Error == "slow_down" {
return true
}
return false
}
// DeviceCode returns a DeviceCode object that can be used to get the code that must be entered on the second
// device and optionally the token once the code has been entered on the second device.
func (t *Client) DeviceCode(ctx context.Context, authParams authority.AuthParams) (DeviceCode, error) {
if err := scopeError(authParams); err != nil {
return DeviceCode{}, err
}
if err := t.resolveEndpoint(ctx, &authParams, ""); err != nil {
return DeviceCode{}, err
}
dcr, err := t.AccessTokens.DeviceCodeResult(ctx, authParams)
if err != nil {
return DeviceCode{}, err
}
return DeviceCode{Result: dcr, authParams: authParams, accessTokens: t.AccessTokens}, nil
}
func (t *Client) resolveEndpoint(ctx context.Context, authParams *authority.AuthParams, userPrincipalName string) error {
endpoints, err := t.Resolver.ResolveEndpoints(ctx, authParams.AuthorityInfo, userPrincipalName)
if err != nil {
return fmt.Errorf("unable to resolve an endpoint: %s", err)
}
authParams.Endpoints = endpoints
return nil
}
// scopeError takes an authority.AuthParams and returns an error
// if len(AuthParams.Scope) == 0.
func scopeError(a authority.AuthParams) error {
// TODO(someone): we could look deeper at the message to determine if
// it's a scope error, but this is a good start.
/*
{error":"invalid_scope","error_description":"AADSTS1002012: The provided value for scope
openid offline_access profile is not valid. Client credential flows must have a scope value
with /.default suffixed to the resource identifier (application ID URI)...}
*/
if len(a.Scopes) == 0 {
return fmt.Errorf("token request had an empty authority.AuthParams.Scopes, which is invalid")
}
return nil
}

View file

@ -0,0 +1,451 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
/*
Package accesstokens exposes a REST client for querying backend systems to get various types of
access tokens (oauth) for use in authentication.
These calls are of type "application/x-www-form-urlencoded". This means we use url.Values to
represent arguments and then encode them into the POST body message. We receive JSON in
return for the requests. The request definition is defined in https://tools.ietf.org/html/rfc7521#section-4.2 .
*/
package accesstokens
import (
"context"
"crypto"
/* #nosec */
"crypto/sha1"
"crypto/x509"
"encoding/base64"
"encoding/json"
"fmt"
"net/url"
"strconv"
"strings"
"time"
"github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/exported"
"github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/oauth/ops/authority"
"github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/oauth/ops/internal/grant"
"github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/oauth/ops/wstrust"
"github.com/golang-jwt/jwt/v4"
"github.com/google/uuid"
)
const (
grantType = "grant_type"
deviceCode = "device_code"
clientID = "client_id"
clientInfo = "client_info"
clientInfoVal = "1"
username = "username"
password = "password"
)
//go:generate stringer -type=AppType
// AppType is whether the authorization code flow is for a public or confidential client.
type AppType int8
const (
// ATUnknown is the zero value when the type hasn't been set.
ATUnknown AppType = iota
// ATPublic indicates this if for the Public.Client.
ATPublic
// ATConfidential indicates this if for the Confidential.Client.
ATConfidential
)
type urlFormCaller interface {
URLFormCall(ctx context.Context, endpoint string, qv url.Values, resp interface{}) error
}
// DeviceCodeResponse represents the HTTP response received from the device code endpoint
type DeviceCodeResponse struct {
authority.OAuthResponseBase
UserCode string `json:"user_code"`
DeviceCode string `json:"device_code"`
VerificationURL string `json:"verification_url"`
ExpiresIn int `json:"expires_in"`
Interval int `json:"interval"`
Message string `json:"message"`
AdditionalFields map[string]interface{}
}
// Convert converts the DeviceCodeResponse to a DeviceCodeResult
func (dcr DeviceCodeResponse) Convert(clientID string, scopes []string) DeviceCodeResult {
expiresOn := time.Now().UTC().Add(time.Duration(dcr.ExpiresIn) * time.Second)
return NewDeviceCodeResult(dcr.UserCode, dcr.DeviceCode, dcr.VerificationURL, expiresOn, dcr.Interval, dcr.Message, clientID, scopes)
}
// Credential represents the credential used in confidential client flows. This can be either
// a Secret or Cert/Key.
type Credential struct {
// Secret contains the credential secret if we are doing auth by secret.
Secret string
// Cert is the public certificate, if we're authenticating by certificate.
Cert *x509.Certificate
// Key is the private key for signing, if we're authenticating by certificate.
Key crypto.PrivateKey
// X5c is the JWT assertion's x5c header value, required for SN/I authentication.
X5c []string
// AssertionCallback is a function provided by the application, if we're authenticating by assertion.
AssertionCallback func(context.Context, exported.AssertionRequestOptions) (string, error)
// TokenProvider is a function provided by the application that implements custom authentication
// logic for a confidential client
TokenProvider func(context.Context, exported.TokenProviderParameters) (exported.TokenProviderResult, error)
}
// JWT gets the jwt assertion when the credential is not using a secret.
func (c *Credential) JWT(ctx context.Context, authParams authority.AuthParams) (string, error) {
if c.AssertionCallback != nil {
options := exported.AssertionRequestOptions{
ClientID: authParams.ClientID,
TokenEndpoint: authParams.Endpoints.TokenEndpoint,
}
return c.AssertionCallback(ctx, options)
}
token := jwt.NewWithClaims(jwt.SigningMethodRS256, jwt.MapClaims{
"aud": authParams.Endpoints.TokenEndpoint,
"exp": json.Number(strconv.FormatInt(time.Now().Add(10*time.Minute).Unix(), 10)),
"iss": authParams.ClientID,
"jti": uuid.New().String(),
"nbf": json.Number(strconv.FormatInt(time.Now().Unix(), 10)),
"sub": authParams.ClientID,
})
token.Header = map[string]interface{}{
"alg": "RS256",
"typ": "JWT",
"x5t": base64.StdEncoding.EncodeToString(thumbprint(c.Cert)),
}
if authParams.SendX5C {
token.Header["x5c"] = c.X5c
}
assertion, err := token.SignedString(c.Key)
if err != nil {
return "", fmt.Errorf("unable to sign a JWT token using private key: %w", err)
}
return assertion, nil
}
// thumbprint runs the asn1.Der bytes through sha1 for use in the x5t parameter of JWT.
// https://tools.ietf.org/html/rfc7517#section-4.8
func thumbprint(cert *x509.Certificate) []byte {
/* #nosec */
a := sha1.Sum(cert.Raw)
return a[:]
}
// Client represents the REST calls to get tokens from token generator backends.
type Client struct {
// Comm provides the HTTP transport client.
Comm urlFormCaller
testing bool
}
// FromUsernamePassword uses a username and password to get an access token.
func (c Client) FromUsernamePassword(ctx context.Context, authParameters authority.AuthParams) (TokenResponse, error) {
qv := url.Values{}
if err := addClaims(qv, authParameters); err != nil {
return TokenResponse{}, err
}
qv.Set(grantType, grant.Password)
qv.Set(username, authParameters.Username)
qv.Set(password, authParameters.Password)
qv.Set(clientID, authParameters.ClientID)
qv.Set(clientInfo, clientInfoVal)
addScopeQueryParam(qv, authParameters)
return c.doTokenResp(ctx, authParameters, qv)
}
// AuthCodeRequest stores the values required to request a token from the authority using an authorization code
type AuthCodeRequest struct {
AuthParams authority.AuthParams
Code string
CodeChallenge string
Credential *Credential
AppType AppType
}
// NewCodeChallengeRequest returns an AuthCodeRequest that uses a code challenge..
func NewCodeChallengeRequest(params authority.AuthParams, appType AppType, cc *Credential, code, challenge string) (AuthCodeRequest, error) {
if appType == ATUnknown {
return AuthCodeRequest{}, fmt.Errorf("bug: NewCodeChallengeRequest() called with AppType == ATUnknown")
}
return AuthCodeRequest{
AuthParams: params,
AppType: appType,
Code: code,
CodeChallenge: challenge,
Credential: cc,
}, nil
}
// FromAuthCode uses an authorization code to retrieve an access token.
func (c Client) FromAuthCode(ctx context.Context, req AuthCodeRequest) (TokenResponse, error) {
var qv url.Values
switch req.AppType {
case ATUnknown:
return TokenResponse{}, fmt.Errorf("bug: Token.AuthCode() received request with AppType == ATUnknown")
case ATConfidential:
var err error
if req.Credential == nil {
return TokenResponse{}, fmt.Errorf("AuthCodeRequest had nil Credential for Confidential app")
}
qv, err = prepURLVals(ctx, req.Credential, req.AuthParams)
if err != nil {
return TokenResponse{}, err
}
case ATPublic:
qv = url.Values{}
default:
return TokenResponse{}, fmt.Errorf("bug: Token.AuthCode() received request with AppType == %v, which we do not recongnize", req.AppType)
}
qv.Set(grantType, grant.AuthCode)
qv.Set("code", req.Code)
qv.Set("code_verifier", req.CodeChallenge)
qv.Set("redirect_uri", req.AuthParams.Redirecturi)
qv.Set(clientID, req.AuthParams.ClientID)
qv.Set(clientInfo, clientInfoVal)
addScopeQueryParam(qv, req.AuthParams)
if err := addClaims(qv, req.AuthParams); err != nil {
return TokenResponse{}, err
}
return c.doTokenResp(ctx, req.AuthParams, qv)
}
// FromRefreshToken uses a refresh token (for refreshing credentials) to get a new access token.
func (c Client) FromRefreshToken(ctx context.Context, appType AppType, authParams authority.AuthParams, cc *Credential, refreshToken string) (TokenResponse, error) {
qv := url.Values{}
if appType == ATConfidential {
var err error
qv, err = prepURLVals(ctx, cc, authParams)
if err != nil {
return TokenResponse{}, err
}
}
if err := addClaims(qv, authParams); err != nil {
return TokenResponse{}, err
}
qv.Set(grantType, grant.RefreshToken)
qv.Set(clientID, authParams.ClientID)
qv.Set(clientInfo, clientInfoVal)
qv.Set("refresh_token", refreshToken)
addScopeQueryParam(qv, authParams)
return c.doTokenResp(ctx, authParams, qv)
}
// FromClientSecret uses a client's secret (aka password) to get a new token.
func (c Client) FromClientSecret(ctx context.Context, authParameters authority.AuthParams, clientSecret string) (TokenResponse, error) {
qv := url.Values{}
if err := addClaims(qv, authParameters); err != nil {
return TokenResponse{}, err
}
qv.Set(grantType, grant.ClientCredential)
qv.Set("client_secret", clientSecret)
qv.Set(clientID, authParameters.ClientID)
addScopeQueryParam(qv, authParameters)
token, err := c.doTokenResp(ctx, authParameters, qv)
if err != nil {
return token, fmt.Errorf("FromClientSecret(): %w", err)
}
return token, nil
}
func (c Client) FromAssertion(ctx context.Context, authParameters authority.AuthParams, assertion string) (TokenResponse, error) {
qv := url.Values{}
if err := addClaims(qv, authParameters); err != nil {
return TokenResponse{}, err
}
qv.Set(grantType, grant.ClientCredential)
qv.Set("client_assertion_type", grant.ClientAssertion)
qv.Set("client_assertion", assertion)
qv.Set(clientID, authParameters.ClientID)
qv.Set(clientInfo, clientInfoVal)
addScopeQueryParam(qv, authParameters)
token, err := c.doTokenResp(ctx, authParameters, qv)
if err != nil {
return token, fmt.Errorf("FromAssertion(): %w", err)
}
return token, nil
}
func (c Client) FromUserAssertionClientSecret(ctx context.Context, authParameters authority.AuthParams, userAssertion string, clientSecret string) (TokenResponse, error) {
qv := url.Values{}
if err := addClaims(qv, authParameters); err != nil {
return TokenResponse{}, err
}
qv.Set(grantType, grant.JWT)
qv.Set(clientID, authParameters.ClientID)
qv.Set("client_secret", clientSecret)
qv.Set("assertion", userAssertion)
qv.Set(clientInfo, clientInfoVal)
qv.Set("requested_token_use", "on_behalf_of")
addScopeQueryParam(qv, authParameters)
return c.doTokenResp(ctx, authParameters, qv)
}
func (c Client) FromUserAssertionClientCertificate(ctx context.Context, authParameters authority.AuthParams, userAssertion string, assertion string) (TokenResponse, error) {
qv := url.Values{}
if err := addClaims(qv, authParameters); err != nil {
return TokenResponse{}, err
}
qv.Set(grantType, grant.JWT)
qv.Set("client_assertion_type", grant.ClientAssertion)
qv.Set("client_assertion", assertion)
qv.Set(clientID, authParameters.ClientID)
qv.Set("assertion", userAssertion)
qv.Set(clientInfo, clientInfoVal)
qv.Set("requested_token_use", "on_behalf_of")
addScopeQueryParam(qv, authParameters)
return c.doTokenResp(ctx, authParameters, qv)
}
func (c Client) DeviceCodeResult(ctx context.Context, authParameters authority.AuthParams) (DeviceCodeResult, error) {
qv := url.Values{}
if err := addClaims(qv, authParameters); err != nil {
return DeviceCodeResult{}, err
}
qv.Set(clientID, authParameters.ClientID)
addScopeQueryParam(qv, authParameters)
endpoint := strings.Replace(authParameters.Endpoints.TokenEndpoint, "token", "devicecode", -1)
resp := DeviceCodeResponse{}
err := c.Comm.URLFormCall(ctx, endpoint, qv, &resp)
if err != nil {
return DeviceCodeResult{}, err
}
return resp.Convert(authParameters.ClientID, authParameters.Scopes), nil
}
func (c Client) FromDeviceCodeResult(ctx context.Context, authParameters authority.AuthParams, deviceCodeResult DeviceCodeResult) (TokenResponse, error) {
qv := url.Values{}
if err := addClaims(qv, authParameters); err != nil {
return TokenResponse{}, err
}
qv.Set(grantType, grant.DeviceCode)
qv.Set(deviceCode, deviceCodeResult.DeviceCode)
qv.Set(clientID, authParameters.ClientID)
qv.Set(clientInfo, clientInfoVal)
addScopeQueryParam(qv, authParameters)
return c.doTokenResp(ctx, authParameters, qv)
}
func (c Client) FromSamlGrant(ctx context.Context, authParameters authority.AuthParams, samlGrant wstrust.SamlTokenInfo) (TokenResponse, error) {
qv := url.Values{}
if err := addClaims(qv, authParameters); err != nil {
return TokenResponse{}, err
}
qv.Set(username, authParameters.Username)
qv.Set(password, authParameters.Password)
qv.Set(clientID, authParameters.ClientID)
qv.Set(clientInfo, clientInfoVal)
qv.Set("assertion", base64.StdEncoding.WithPadding(base64.StdPadding).EncodeToString([]byte(samlGrant.Assertion)))
addScopeQueryParam(qv, authParameters)
switch samlGrant.AssertionType {
case grant.SAMLV1:
qv.Set(grantType, grant.SAMLV1)
case grant.SAMLV2:
qv.Set(grantType, grant.SAMLV2)
default:
return TokenResponse{}, fmt.Errorf("GetAccessTokenFromSamlGrant returned unknown SAML assertion type: %q", samlGrant.AssertionType)
}
return c.doTokenResp(ctx, authParameters, qv)
}
func (c Client) doTokenResp(ctx context.Context, authParams authority.AuthParams, qv url.Values) (TokenResponse, error) {
resp := TokenResponse{}
err := c.Comm.URLFormCall(ctx, authParams.Endpoints.TokenEndpoint, qv, &resp)
if err != nil {
return resp, err
}
resp.ComputeScope(authParams)
if c.testing {
return resp, nil
}
return resp, resp.Validate()
}
// prepURLVals returns an url.Values that sets various key/values if we are doing secrets
// or JWT assertions.
func prepURLVals(ctx context.Context, cc *Credential, authParams authority.AuthParams) (url.Values, error) {
params := url.Values{}
if cc.Secret != "" {
params.Set("client_secret", cc.Secret)
return params, nil
}
jwt, err := cc.JWT(ctx, authParams)
if err != nil {
return nil, err
}
params.Set("client_assertion", jwt)
params.Set("client_assertion_type", grant.ClientAssertion)
return params, nil
}
// openid required to get an id token
// offline_access required to get a refresh token
// profile required to get the client_info field back
var detectDefaultScopes = map[string]bool{
"openid": true,
"offline_access": true,
"profile": true,
}
var defaultScopes = []string{"openid", "offline_access", "profile"}
func AppendDefaultScopes(authParameters authority.AuthParams) []string {
scopes := make([]string, 0, len(authParameters.Scopes)+len(defaultScopes))
for _, scope := range authParameters.Scopes {
s := strings.TrimSpace(scope)
if s == "" {
continue
}
if detectDefaultScopes[scope] {
continue
}
scopes = append(scopes, scope)
}
scopes = append(scopes, defaultScopes...)
return scopes
}
// addClaims adds client capabilities and claims from AuthParams to the given url.Values
func addClaims(v url.Values, ap authority.AuthParams) error {
claims, err := ap.MergeCapabilitiesAndClaims()
if err == nil && claims != "" {
v.Set("claims", claims)
}
return err
}
func addScopeQueryParam(queryParams url.Values, authParameters authority.AuthParams) {
scopes := AppendDefaultScopes(authParameters)
queryParams.Set("scope", strings.Join(scopes, " "))
}

View file

@ -0,0 +1,25 @@
// Code generated by "stringer -type=AppType"; DO NOT EDIT.
package accesstokens
import "strconv"
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
var x [1]struct{}
_ = x[ATUnknown-0]
_ = x[ATPublic-1]
_ = x[ATConfidential-2]
}
const _AppType_name = "ATUnknownATPublicATConfidential"
var _AppType_index = [...]uint8{0, 9, 17, 31}
func (i AppType) String() string {
if i < 0 || i >= AppType(len(_AppType_index)-1) {
return "AppType(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _AppType_name[_AppType_index[i]:_AppType_index[i+1]]
}

View file

@ -0,0 +1,335 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
package accesstokens
import (
"bytes"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"reflect"
"strings"
"time"
internalTime "github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/json/types/time"
"github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/oauth/ops/authority"
"github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/shared"
)
// IDToken consists of all the information used to validate a user.
// https://docs.microsoft.com/azure/active-directory/develop/id-tokens .
type IDToken struct {
PreferredUsername string `json:"preferred_username,omitempty"`
GivenName string `json:"given_name,omitempty"`
FamilyName string `json:"family_name,omitempty"`
MiddleName string `json:"middle_name,omitempty"`
Name string `json:"name,omitempty"`
Oid string `json:"oid,omitempty"`
TenantID string `json:"tid,omitempty"`
Subject string `json:"sub,omitempty"`
UPN string `json:"upn,omitempty"`
Email string `json:"email,omitempty"`
AlternativeID string `json:"alternative_id,omitempty"`
Issuer string `json:"iss,omitempty"`
Audience string `json:"aud,omitempty"`
ExpirationTime int64 `json:"exp,omitempty"`
IssuedAt int64 `json:"iat,omitempty"`
NotBefore int64 `json:"nbf,omitempty"`
RawToken string
AdditionalFields map[string]interface{}
}
var null = []byte("null")
// UnmarshalJSON implements json.Unmarshaler.
func (i *IDToken) UnmarshalJSON(b []byte) error {
if bytes.Equal(null, b) {
return nil
}
// Because we have a custom unmarshaler, you
// cannot directly call json.Unmarshal here. If you do, it will call this function
// recursively until reach our recursion limit. We have to create a new type
// that doesn't have this method in order to use json.Unmarshal.
type idToken2 IDToken
jwt := strings.Trim(string(b), `"`)
jwtArr := strings.Split(jwt, ".")
if len(jwtArr) < 2 {
return errors.New("IDToken returned from server is invalid")
}
jwtPart := jwtArr[1]
jwtDecoded, err := decodeJWT(jwtPart)
if err != nil {
return fmt.Errorf("unable to unmarshal IDToken, problem decoding JWT: %w", err)
}
token := idToken2{}
err = json.Unmarshal(jwtDecoded, &token)
if err != nil {
return fmt.Errorf("unable to unmarshal IDToken: %w", err)
}
token.RawToken = jwt
*i = IDToken(token)
return nil
}
// IsZero indicates if the IDToken is the zero value.
func (i IDToken) IsZero() bool {
v := reflect.ValueOf(i)
for i := 0; i < v.NumField(); i++ {
field := v.Field(i)
if !field.IsZero() {
switch field.Kind() {
case reflect.Map, reflect.Slice:
if field.Len() == 0 {
continue
}
}
return false
}
}
return true
}
// LocalAccountID extracts an account's local account ID from an ID token.
func (i IDToken) LocalAccountID() string {
if i.Oid != "" {
return i.Oid
}
return i.Subject
}
// jwtDecoder is provided to allow tests to provide their own.
var jwtDecoder = decodeJWT
// ClientInfo is used to create a Home Account ID for an account.
type ClientInfo struct {
UID string `json:"uid"`
UTID string `json:"utid"`
AdditionalFields map[string]interface{}
}
// UnmarshalJSON implements json.Unmarshaler.s
func (c *ClientInfo) UnmarshalJSON(b []byte) error {
s := strings.Trim(string(b), `"`)
// Client info may be empty in some flows, e.g. certificate exchange.
if len(s) == 0 {
return nil
}
// Because we have a custom unmarshaler, you
// cannot directly call json.Unmarshal here. If you do, it will call this function
// recursively until reach our recursion limit. We have to create a new type
// that doesn't have this method in order to use json.Unmarshal.
type clientInfo2 ClientInfo
raw, err := jwtDecoder(s)
if err != nil {
return fmt.Errorf("TokenResponse client_info field had JWT decode error: %w", err)
}
var c2 clientInfo2
err = json.Unmarshal(raw, &c2)
if err != nil {
return fmt.Errorf("was unable to unmarshal decoded JWT in TokenRespone to ClientInfo: %w", err)
}
*c = ClientInfo(c2)
return nil
}
// HomeAccountID creates the home account ID.
func (c ClientInfo) HomeAccountID() string {
if c.UID == "" {
return ""
} else if c.UTID == "" {
return fmt.Sprintf("%s.%s", c.UID, c.UID)
} else {
return fmt.Sprintf("%s.%s", c.UID, c.UTID)
}
}
// Scopes represents scopes in a TokenResponse.
type Scopes struct {
Slice []string
}
// UnmarshalJSON implements json.Unmarshal.
func (s *Scopes) UnmarshalJSON(b []byte) error {
str := strings.Trim(string(b), `"`)
if len(str) == 0 {
return nil
}
sl := strings.Split(str, " ")
s.Slice = sl
return nil
}
// TokenResponse is the information that is returned from a token endpoint during a token acquisition flow.
type TokenResponse struct {
authority.OAuthResponseBase
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
FamilyID string `json:"foci"`
IDToken IDToken `json:"id_token"`
ClientInfo ClientInfo `json:"client_info"`
ExpiresOn internalTime.DurationTime `json:"expires_in"`
ExtExpiresOn internalTime.DurationTime `json:"ext_expires_in"`
GrantedScopes Scopes `json:"scope"`
DeclinedScopes []string // This is derived
AdditionalFields map[string]interface{}
scopesComputed bool
}
// ComputeScope computes the final scopes based on what was granted by the server and
// what our AuthParams were from the authority server. Per OAuth spec, if no scopes are returned, the response should be treated as if all scopes were granted
// This behavior can be observed in client assertion flows, but can happen at any time, this check ensures we treat
// those special responses properly Link to spec: https://tools.ietf.org/html/rfc6749#section-3.3
func (tr *TokenResponse) ComputeScope(authParams authority.AuthParams) {
if len(tr.GrantedScopes.Slice) == 0 {
tr.GrantedScopes = Scopes{Slice: authParams.Scopes}
} else {
tr.DeclinedScopes = findDeclinedScopes(authParams.Scopes, tr.GrantedScopes.Slice)
}
tr.scopesComputed = true
}
// Validate validates the TokenResponse has basic valid values. It must be called
// after ComputeScopes() is called.
func (tr *TokenResponse) Validate() error {
if tr.Error != "" {
return fmt.Errorf("%s: %s", tr.Error, tr.ErrorDescription)
}
if tr.AccessToken == "" {
return errors.New("response is missing access_token")
}
if !tr.scopesComputed {
return fmt.Errorf("TokenResponse hasn't had ScopesComputed() called")
}
return nil
}
func (tr *TokenResponse) CacheKey(authParams authority.AuthParams) string {
if authParams.AuthorizationType == authority.ATOnBehalfOf {
return authParams.AssertionHash()
}
if authParams.AuthorizationType == authority.ATClientCredentials {
return authParams.AppKey()
}
if authParams.IsConfidentialClient || authParams.AuthorizationType == authority.ATRefreshToken {
return tr.ClientInfo.HomeAccountID()
}
return ""
}
func findDeclinedScopes(requestedScopes []string, grantedScopes []string) []string {
declined := []string{}
grantedMap := map[string]bool{}
for _, s := range grantedScopes {
grantedMap[strings.ToLower(s)] = true
}
// Comparing the requested scopes with the granted scopes to see if there are any scopes that have been declined.
for _, r := range requestedScopes {
if !grantedMap[strings.ToLower(r)] {
declined = append(declined, r)
}
}
return declined
}
// decodeJWT decodes a JWT and converts it to a byte array representing a JSON object
// JWT has headers and payload base64url encoded without padding
// https://tools.ietf.org/html/rfc7519#section-3 and
// https://tools.ietf.org/html/rfc7515#section-2
func decodeJWT(data string) ([]byte, error) {
// https://tools.ietf.org/html/rfc7515#appendix-C
return base64.RawURLEncoding.DecodeString(data)
}
// RefreshToken is the JSON representation of a MSAL refresh token for encoding to storage.
type RefreshToken struct {
HomeAccountID string `json:"home_account_id,omitempty"`
Environment string `json:"environment,omitempty"`
CredentialType string `json:"credential_type,omitempty"`
ClientID string `json:"client_id,omitempty"`
FamilyID string `json:"family_id,omitempty"`
Secret string `json:"secret,omitempty"`
Realm string `json:"realm,omitempty"`
Target string `json:"target,omitempty"`
UserAssertionHash string `json:"user_assertion_hash,omitempty"`
AdditionalFields map[string]interface{}
}
// NewRefreshToken is the constructor for RefreshToken.
func NewRefreshToken(homeID, env, clientID, refreshToken, familyID string) RefreshToken {
return RefreshToken{
HomeAccountID: homeID,
Environment: env,
CredentialType: "RefreshToken",
ClientID: clientID,
FamilyID: familyID,
Secret: refreshToken,
}
}
// Key outputs the key that can be used to uniquely look up this entry in a map.
func (rt RefreshToken) Key() string {
var fourth = rt.FamilyID
if fourth == "" {
fourth = rt.ClientID
}
return strings.Join(
[]string{rt.HomeAccountID, rt.Environment, rt.CredentialType, fourth},
shared.CacheKeySeparator,
)
}
func (rt RefreshToken) GetSecret() string {
return rt.Secret
}
// DeviceCodeResult stores the response from the STS device code endpoint.
type DeviceCodeResult struct {
// UserCode is the code the user needs to provide when authentication at the verification URI.
UserCode string
// DeviceCode is the code used in the access token request.
DeviceCode string
// VerificationURL is the the URL where user can authenticate.
VerificationURL string
// ExpiresOn is the expiration time of device code in seconds.
ExpiresOn time.Time
// Interval is the interval at which the STS should be polled at.
Interval int
// Message is the message which should be displayed to the user.
Message string
// ClientID is the UUID issued by the authorization server for your application.
ClientID string
// Scopes is the OpenID scopes used to request access a protected API.
Scopes []string
}
// NewDeviceCodeResult creates a DeviceCodeResult instance.
func NewDeviceCodeResult(userCode, deviceCode, verificationURL string, expiresOn time.Time, interval int, message, clientID string, scopes []string) DeviceCodeResult {
return DeviceCodeResult{userCode, deviceCode, verificationURL, expiresOn, interval, message, clientID, scopes}
}
func (dcr DeviceCodeResult) String() string {
return fmt.Sprintf("UserCode: (%v)\nDeviceCode: (%v)\nURL: (%v)\nMessage: (%v)\n", dcr.UserCode, dcr.DeviceCode, dcr.VerificationURL, dcr.Message)
}

View file

@ -0,0 +1,552 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
package authority
import (
"context"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"os"
"path"
"strings"
"time"
"github.com/google/uuid"
)
const (
authorizationEndpoint = "https://%v/%v/oauth2/v2.0/authorize"
instanceDiscoveryEndpoint = "https://%v/common/discovery/instance"
tenantDiscoveryEndpointWithRegion = "https://%s.%s/%s/v2.0/.well-known/openid-configuration"
regionName = "REGION_NAME"
defaultAPIVersion = "2021-10-01"
imdsEndpoint = "http://169.254.169.254/metadata/instance/compute/location?format=text&api-version=" + defaultAPIVersion
autoDetectRegion = "TryAutoDetect"
)
// These are various hosts that host AAD Instance discovery endpoints.
const (
defaultHost = "login.microsoftonline.com"
loginMicrosoft = "login.microsoft.com"
loginWindows = "login.windows.net"
loginSTSWindows = "sts.windows.net"
loginMicrosoftOnline = defaultHost
)
// jsonCaller is an interface that allows us to mock the JSONCall method.
type jsonCaller interface {
JSONCall(ctx context.Context, endpoint string, headers http.Header, qv url.Values, body, resp interface{}) error
}
var aadTrustedHostList = map[string]bool{
"login.windows.net": true, // Microsoft Azure Worldwide - Used in validation scenarios where host is not this list
"login.chinacloudapi.cn": true, // Microsoft Azure China
"login.microsoftonline.de": true, // Microsoft Azure Blackforest
"login-us.microsoftonline.com": true, // Microsoft Azure US Government - Legacy
"login.microsoftonline.us": true, // Microsoft Azure US Government
"login.microsoftonline.com": true, // Microsoft Azure Worldwide
"login.cloudgovapi.us": true, // Microsoft Azure US Government
}
// TrustedHost checks if an AAD host is trusted/valid.
func TrustedHost(host string) bool {
if _, ok := aadTrustedHostList[host]; ok {
return true
}
return false
}
// OAuthResponseBase is the base JSON return message for an OAuth call.
// This is embedded in other calls to get the base fields from every response.
type OAuthResponseBase struct {
Error string `json:"error"`
SubError string `json:"suberror"`
ErrorDescription string `json:"error_description"`
ErrorCodes []int `json:"error_codes"`
CorrelationID string `json:"correlation_id"`
Claims string `json:"claims"`
}
// TenantDiscoveryResponse is the tenant endpoints from the OpenID configuration endpoint.
type TenantDiscoveryResponse struct {
OAuthResponseBase
AuthorizationEndpoint string `json:"authorization_endpoint"`
TokenEndpoint string `json:"token_endpoint"`
Issuer string `json:"issuer"`
AdditionalFields map[string]interface{}
}
// Validate validates that the response had the correct values required.
func (r *TenantDiscoveryResponse) Validate() error {
switch "" {
case r.AuthorizationEndpoint:
return errors.New("TenantDiscoveryResponse: authorize endpoint was not found in the openid configuration")
case r.TokenEndpoint:
return errors.New("TenantDiscoveryResponse: token endpoint was not found in the openid configuration")
case r.Issuer:
return errors.New("TenantDiscoveryResponse: issuer was not found in the openid configuration")
}
return nil
}
type InstanceDiscoveryMetadata struct {
PreferredNetwork string `json:"preferred_network"`
PreferredCache string `json:"preferred_cache"`
Aliases []string `json:"aliases"`
AdditionalFields map[string]interface{}
}
type InstanceDiscoveryResponse struct {
TenantDiscoveryEndpoint string `json:"tenant_discovery_endpoint"`
Metadata []InstanceDiscoveryMetadata `json:"metadata"`
AdditionalFields map[string]interface{}
}
//go:generate stringer -type=AuthorizeType
// AuthorizeType represents the type of token flow.
type AuthorizeType int
// These are all the types of token flows.
const (
ATUnknown AuthorizeType = iota
ATUsernamePassword
ATWindowsIntegrated
ATAuthCode
ATInteractive
ATClientCredentials
ATDeviceCode
ATRefreshToken
AccountByID
ATOnBehalfOf
)
// These are all authority types
const (
AAD = "MSSTS"
ADFS = "ADFS"
)
// AuthParams represents the parameters used for authorization for token acquisition.
type AuthParams struct {
AuthorityInfo Info
CorrelationID string
Endpoints Endpoints
ClientID string
// Redirecturi is used for auth flows that specify a redirect URI (e.g. local server for interactive auth flow).
Redirecturi string
HomeAccountID string
// Username is the user-name portion for username/password auth flow.
Username string
// Password is the password portion for username/password auth flow.
Password string
// Scopes is the list of scopes the user consents to.
Scopes []string
// AuthorizationType specifies the auth flow being used.
AuthorizationType AuthorizeType
// State is a random value used to prevent cross-site request forgery attacks.
State string
// CodeChallenge is derived from a code verifier and is sent in the auth request.
CodeChallenge string
// CodeChallengeMethod describes the method used to create the CodeChallenge.
CodeChallengeMethod string
// Prompt specifies the user prompt type during interactive auth.
Prompt string
// IsConfidentialClient specifies if it is a confidential client.
IsConfidentialClient bool
// SendX5C specifies if x5c claim(public key of the certificate) should be sent to STS.
SendX5C bool
// UserAssertion is the access token used to acquire token on behalf of user
UserAssertion string
// Capabilities the client will include with each token request, for example "CP1".
// Call [NewClientCapabilities] to construct a value for this field.
Capabilities ClientCapabilities
// Claims required for an access token to satisfy a conditional access policy
Claims string
// KnownAuthorityHosts don't require metadata discovery because they're known to the user
KnownAuthorityHosts []string
// LoginHint is a username with which to pre-populate account selection during interactive auth
LoginHint string
// DomainHint is a directive that can be used to accelerate the user to their federated IdP sign-in page
DomainHint string
}
// NewAuthParams creates an authorization parameters object.
func NewAuthParams(clientID string, authorityInfo Info) AuthParams {
return AuthParams{
ClientID: clientID,
AuthorityInfo: authorityInfo,
CorrelationID: uuid.New().String(),
}
}
// WithTenant returns a copy of the AuthParams having the specified tenant ID. If the given
// ID is empty, the copy is identical to the original. This function returns an error in
// several cases:
// - ID isn't specific (for example, it's "common")
// - ID is non-empty and the authority doesn't support tenants (for example, it's an ADFS authority)
// - the client is configured to authenticate only Microsoft accounts via the "consumers" endpoint
// - the resulting authority URL is invalid
func (p AuthParams) WithTenant(ID string) (AuthParams, error) {
switch ID {
case "", p.AuthorityInfo.Tenant:
// keep the default tenant because the caller didn't override it
return p, nil
case "common", "consumers", "organizations":
if p.AuthorityInfo.AuthorityType == AAD {
return p, fmt.Errorf(`tenant ID must be a specific tenant, not "%s"`, ID)
}
// else we'll return a better error below
}
if p.AuthorityInfo.AuthorityType != AAD {
return p, errors.New("the authority doesn't support tenants")
}
if p.AuthorityInfo.Tenant == "consumers" {
return p, errors.New(`client is configured to authenticate only personal Microsoft accounts, via the "consumers" endpoint`)
}
authority := "https://" + path.Join(p.AuthorityInfo.Host, ID)
info, err := NewInfoFromAuthorityURI(authority, p.AuthorityInfo.ValidateAuthority, p.AuthorityInfo.InstanceDiscoveryDisabled)
if err == nil {
info.Region = p.AuthorityInfo.Region
p.AuthorityInfo = info
}
return p, err
}
// MergeCapabilitiesAndClaims combines client capabilities and challenge claims into a value suitable for an authentication request's "claims" parameter.
func (p AuthParams) MergeCapabilitiesAndClaims() (string, error) {
claims := p.Claims
if len(p.Capabilities.asMap) > 0 {
if claims == "" {
// without claims the result is simply the capabilities
return p.Capabilities.asJSON, nil
}
// Otherwise, merge claims and capabilties into a single JSON object.
// We handle the claims challenge as a map because we don't know its structure.
var challenge map[string]any
if err := json.Unmarshal([]byte(claims), &challenge); err != nil {
return "", fmt.Errorf(`claims must be JSON. Are they base64 encoded? json.Unmarshal returned "%v"`, err)
}
if err := merge(p.Capabilities.asMap, challenge); err != nil {
return "", err
}
b, err := json.Marshal(challenge)
if err != nil {
return "", err
}
claims = string(b)
}
return claims, nil
}
// merges a into b without overwriting b's values. Returns an error when a and b share a key for which either has a non-object value.
func merge(a, b map[string]any) error {
for k, av := range a {
if bv, ok := b[k]; !ok {
// b doesn't contain this key => simply set it to a's value
b[k] = av
} else {
// b does contain this key => recursively merge a[k] into b[k], provided both are maps. If a[k] or b[k] isn't
// a map, return an error because merging would overwrite some value in b. Errors shouldn't occur in practice
// because the challenge will be from AAD, which knows the capabilities format.
if A, ok := av.(map[string]any); ok {
if B, ok := bv.(map[string]any); ok {
return merge(A, B)
} else {
// b[k] isn't a map
return errors.New("challenge claims conflict with client capabilities")
}
} else {
// a[k] isn't a map
return errors.New("challenge claims conflict with client capabilities")
}
}
}
return nil
}
// ClientCapabilities stores capabilities in the formats used by AuthParams.MergeCapabilitiesAndClaims.
// [NewClientCapabilities] precomputes these representations because capabilities are static for the
// lifetime of a client and are included with every authentication request i.e., these computations
// always have the same result and would otherwise have to be repeated for every request.
type ClientCapabilities struct {
// asJSON is for the common case: adding the capabilities to an auth request with no challenge claims
asJSON string
// asMap is for merging the capabilities with challenge claims
asMap map[string]any
}
func NewClientCapabilities(capabilities []string) (ClientCapabilities, error) {
c := ClientCapabilities{}
var err error
if len(capabilities) > 0 {
cpbs := make([]string, len(capabilities))
for i := 0; i < len(cpbs); i++ {
cpbs[i] = fmt.Sprintf(`"%s"`, capabilities[i])
}
c.asJSON = fmt.Sprintf(`{"access_token":{"xms_cc":{"values":[%s]}}}`, strings.Join(cpbs, ","))
// note our JSON is valid but we can't stop users breaking it with garbage like "}"
err = json.Unmarshal([]byte(c.asJSON), &c.asMap)
}
return c, err
}
// Info consists of information about the authority.
type Info struct {
Host string
CanonicalAuthorityURI string
AuthorityType string
UserRealmURIPrefix string
ValidateAuthority bool
Tenant string
Region string
InstanceDiscoveryDisabled bool
}
func firstPathSegment(u *url.URL) (string, error) {
pathParts := strings.Split(u.EscapedPath(), "/")
if len(pathParts) >= 2 {
return pathParts[1], nil
}
return "", errors.New(`authority must be an https URL such as "https://login.microsoftonline.com/<your tenant>"`)
}
// NewInfoFromAuthorityURI creates an AuthorityInfo instance from the authority URL provided.
func NewInfoFromAuthorityURI(authority string, validateAuthority bool, instanceDiscoveryDisabled bool) (Info, error) {
u, err := url.Parse(strings.ToLower(authority))
if err != nil || u.Scheme != "https" {
return Info{}, errors.New(`authority must be an https URL such as "https://login.microsoftonline.com/<your tenant>"`)
}
tenant, err := firstPathSegment(u)
if err != nil {
return Info{}, err
}
authorityType := AAD
if tenant == "adfs" {
authorityType = ADFS
}
// u.Host includes the port, if any, which is required for private cloud deployments
return Info{
Host: u.Host,
CanonicalAuthorityURI: fmt.Sprintf("https://%v/%v/", u.Host, tenant),
AuthorityType: authorityType,
UserRealmURIPrefix: fmt.Sprintf("https://%v/common/userrealm/", u.Hostname()),
ValidateAuthority: validateAuthority,
Tenant: tenant,
InstanceDiscoveryDisabled: instanceDiscoveryDisabled,
}, nil
}
// Endpoints consists of the endpoints from the tenant discovery response.
type Endpoints struct {
AuthorizationEndpoint string
TokenEndpoint string
selfSignedJwtAudience string
authorityHost string
}
// NewEndpoints creates an Endpoints object.
func NewEndpoints(authorizationEndpoint string, tokenEndpoint string, selfSignedJwtAudience string, authorityHost string) Endpoints {
return Endpoints{authorizationEndpoint, tokenEndpoint, selfSignedJwtAudience, authorityHost}
}
// UserRealmAccountType refers to the type of user realm.
type UserRealmAccountType string
// These are the different types of user realms.
const (
Unknown UserRealmAccountType = ""
Federated UserRealmAccountType = "Federated"
Managed UserRealmAccountType = "Managed"
)
// UserRealm is used for the username password request to determine user type
type UserRealm struct {
AccountType UserRealmAccountType `json:"account_type"`
DomainName string `json:"domain_name"`
CloudInstanceName string `json:"cloud_instance_name"`
CloudAudienceURN string `json:"cloud_audience_urn"`
// required if accountType is Federated
FederationProtocol string `json:"federation_protocol"`
FederationMetadataURL string `json:"federation_metadata_url"`
AdditionalFields map[string]interface{}
}
func (u UserRealm) validate() error {
switch "" {
case string(u.AccountType):
return errors.New("the account type (Federated or Managed) is missing")
case u.DomainName:
return errors.New("domain name of user realm is missing")
case u.CloudInstanceName:
return errors.New("cloud instance name of user realm is missing")
case u.CloudAudienceURN:
return errors.New("cloud Instance URN is missing")
}
if u.AccountType == Federated {
switch "" {
case u.FederationProtocol:
return errors.New("federation protocol of user realm is missing")
case u.FederationMetadataURL:
return errors.New("federation metadata URL of user realm is missing")
}
}
return nil
}
// Client represents the REST calls to authority backends.
type Client struct {
// Comm provides the HTTP transport client.
Comm jsonCaller // *comm.Client
}
func (c Client) UserRealm(ctx context.Context, authParams AuthParams) (UserRealm, error) {
endpoint := fmt.Sprintf("https://%s/common/UserRealm/%s", authParams.Endpoints.authorityHost, url.PathEscape(authParams.Username))
qv := url.Values{
"api-version": []string{"1.0"},
}
resp := UserRealm{}
err := c.Comm.JSONCall(
ctx,
endpoint,
http.Header{"client-request-id": []string{authParams.CorrelationID}},
qv,
nil,
&resp,
)
if err != nil {
return resp, err
}
return resp, resp.validate()
}
func (c Client) GetTenantDiscoveryResponse(ctx context.Context, openIDConfigurationEndpoint string) (TenantDiscoveryResponse, error) {
resp := TenantDiscoveryResponse{}
err := c.Comm.JSONCall(
ctx,
openIDConfigurationEndpoint,
http.Header{},
nil,
nil,
&resp,
)
return resp, err
}
// AADInstanceDiscovery attempts to discover a tenant endpoint (used in OIDC auth with an authorization endpoint).
// This is done by AAD which allows for aliasing of tenants (windows.sts.net is the same as login.windows.com).
func (c Client) AADInstanceDiscovery(ctx context.Context, authorityInfo Info) (InstanceDiscoveryResponse, error) {
region := ""
var err error
resp := InstanceDiscoveryResponse{}
if authorityInfo.Region != "" && authorityInfo.Region != autoDetectRegion {
region = authorityInfo.Region
} else if authorityInfo.Region == autoDetectRegion {
region = detectRegion(ctx)
}
if region != "" {
environment := authorityInfo.Host
switch environment {
case loginMicrosoft, loginWindows, loginSTSWindows, defaultHost:
environment = loginMicrosoft
}
resp.TenantDiscoveryEndpoint = fmt.Sprintf(tenantDiscoveryEndpointWithRegion, region, environment, authorityInfo.Tenant)
metadata := InstanceDiscoveryMetadata{
PreferredNetwork: fmt.Sprintf("%v.%v", region, authorityInfo.Host),
PreferredCache: authorityInfo.Host,
Aliases: []string{fmt.Sprintf("%v.%v", region, authorityInfo.Host), authorityInfo.Host},
}
resp.Metadata = []InstanceDiscoveryMetadata{metadata}
} else {
qv := url.Values{}
qv.Set("api-version", "1.1")
qv.Set("authorization_endpoint", fmt.Sprintf(authorizationEndpoint, authorityInfo.Host, authorityInfo.Tenant))
discoveryHost := defaultHost
if TrustedHost(authorityInfo.Host) {
discoveryHost = authorityInfo.Host
}
endpoint := fmt.Sprintf(instanceDiscoveryEndpoint, discoveryHost)
err = c.Comm.JSONCall(ctx, endpoint, http.Header{}, qv, nil, &resp)
}
return resp, err
}
func detectRegion(ctx context.Context) string {
region := os.Getenv(regionName)
if region != "" {
region = strings.ReplaceAll(region, " ", "")
return strings.ToLower(region)
}
// HTTP call to IMDS endpoint to get region
// Refer : https://identitydivision.visualstudio.com/DevEx/_git/AuthLibrariesApiReview?path=%2FPinAuthToRegion%2FAAD%20SDK%20Proposal%20to%20Pin%20Auth%20to%20region.md&_a=preview&version=GBdev
// Set a 2 second timeout for this http client which only does calls to IMDS endpoint
client := http.Client{
Timeout: time.Duration(2 * time.Second),
}
req, _ := http.NewRequest("GET", imdsEndpoint, nil)
req.Header.Set("Metadata", "true")
resp, err := client.Do(req)
// If the request times out or there is an error, it is retried once
if err != nil || resp.StatusCode != 200 {
resp, err = client.Do(req)
if err != nil || resp.StatusCode != 200 {
return ""
}
}
defer resp.Body.Close()
response, err := io.ReadAll(resp.Body)
if err != nil {
return ""
}
return string(response)
}
func (a *AuthParams) CacheKey(isAppCache bool) string {
if a.AuthorizationType == ATOnBehalfOf {
return a.AssertionHash()
}
if a.AuthorizationType == ATClientCredentials || isAppCache {
return a.AppKey()
}
if a.AuthorizationType == ATRefreshToken || a.AuthorizationType == AccountByID {
return a.HomeAccountID
}
return ""
}
func (a *AuthParams) AssertionHash() string {
hasher := sha256.New()
// Per documentation this never returns an error : https://pkg.go.dev/hash#pkg-types
_, _ = hasher.Write([]byte(a.UserAssertion))
sha := base64.URLEncoding.EncodeToString(hasher.Sum(nil))
return sha
}
func (a *AuthParams) AppKey() string {
if a.AuthorityInfo.Tenant != "" {
return fmt.Sprintf("%s_%s_AppTokenCache", a.ClientID, a.AuthorityInfo.Tenant)
}
return fmt.Sprintf("%s__AppTokenCache", a.ClientID)
}

View file

@ -0,0 +1,30 @@
// Code generated by "stringer -type=AuthorizeType"; DO NOT EDIT.
package authority
import "strconv"
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
var x [1]struct{}
_ = x[ATUnknown-0]
_ = x[ATUsernamePassword-1]
_ = x[ATWindowsIntegrated-2]
_ = x[ATAuthCode-3]
_ = x[ATInteractive-4]
_ = x[ATClientCredentials-5]
_ = x[ATDeviceCode-6]
_ = x[ATRefreshToken-7]
}
const _AuthorizeType_name = "ATUnknownATUsernamePasswordATWindowsIntegratedATAuthCodeATInteractiveATClientCredentialsATDeviceCodeATRefreshToken"
var _AuthorizeType_index = [...]uint8{0, 9, 27, 46, 56, 69, 88, 100, 114}
func (i AuthorizeType) String() string {
if i < 0 || i >= AuthorizeType(len(_AuthorizeType_index)-1) {
return "AuthorizeType(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _AuthorizeType_name[_AuthorizeType_index[i]:_AuthorizeType_index[i+1]]
}

View file

@ -0,0 +1,320 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
// Package comm provides helpers for communicating with HTTP backends.
package comm
import (
"bytes"
"context"
"encoding/json"
"encoding/xml"
"fmt"
"io"
"net/http"
"net/url"
"reflect"
"runtime"
"strings"
"time"
"github.com/AzureAD/microsoft-authentication-library-for-go/apps/errors"
customJSON "github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/json"
"github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/version"
"github.com/google/uuid"
)
// HTTPClient represents an HTTP client.
// It's usually an *http.Client from the standard library.
type HTTPClient interface {
// Do sends an HTTP request and returns an HTTP response.
Do(req *http.Request) (*http.Response, error)
// CloseIdleConnections closes any idle connections in a "keep-alive" state.
CloseIdleConnections()
}
// Client provides a wrapper to our *http.Client that handles compression and serialization needs.
type Client struct {
client HTTPClient
}
// New returns a new Client object.
func New(httpClient HTTPClient) *Client {
if httpClient == nil {
panic("http.Client cannot == nil")
}
return &Client{client: httpClient}
}
// JSONCall connects to the REST endpoint passing the HTTP query values, headers and JSON conversion
// of body in the HTTP body. It automatically handles compression and decompression with gzip. The response is JSON
// unmarshalled into resp. resp must be a pointer to a struct. If the body struct contains a field called
// "AdditionalFields" we use a custom marshal/unmarshal engine.
func (c *Client) JSONCall(ctx context.Context, endpoint string, headers http.Header, qv url.Values, body, resp interface{}) error {
if qv == nil {
qv = url.Values{}
}
v := reflect.ValueOf(resp)
if err := c.checkResp(v); err != nil {
return err
}
// Choose a JSON marshal/unmarshal depending on if we have AdditionalFields attribute.
var marshal = json.Marshal
var unmarshal = json.Unmarshal
if _, ok := v.Elem().Type().FieldByName("AdditionalFields"); ok {
marshal = customJSON.Marshal
unmarshal = customJSON.Unmarshal
}
u, err := url.Parse(endpoint)
if err != nil {
return fmt.Errorf("could not parse path URL(%s): %w", endpoint, err)
}
u.RawQuery = qv.Encode()
addStdHeaders(headers)
req := &http.Request{Method: http.MethodGet, URL: u, Header: headers}
if body != nil {
// Note: In case your wondering why we are not gzip encoding....
// I'm not sure if these various services support gzip on send.
headers.Add("Content-Type", "application/json; charset=utf-8")
data, err := marshal(body)
if err != nil {
return fmt.Errorf("bug: conn.Call(): could not marshal the body object: %w", err)
}
req.Body = io.NopCloser(bytes.NewBuffer(data))
req.Method = http.MethodPost
}
data, err := c.do(ctx, req)
if err != nil {
return err
}
if resp != nil {
if err := unmarshal(data, resp); err != nil {
return fmt.Errorf("json decode error: %w\njson message bytes were: %s", err, string(data))
}
}
return nil
}
// XMLCall connects to an endpoint and decodes the XML response into resp. This is used when
// sending application/xml . If sending XML via SOAP, use SOAPCall().
func (c *Client) XMLCall(ctx context.Context, endpoint string, headers http.Header, qv url.Values, resp interface{}) error {
if err := c.checkResp(reflect.ValueOf(resp)); err != nil {
return err
}
if qv == nil {
qv = url.Values{}
}
u, err := url.Parse(endpoint)
if err != nil {
return fmt.Errorf("could not parse path URL(%s): %w", endpoint, err)
}
u.RawQuery = qv.Encode()
headers.Set("Content-Type", "application/xml; charset=utf-8") // This was not set in he original Mex(), but...
addStdHeaders(headers)
return c.xmlCall(ctx, u, headers, "", resp)
}
// SOAPCall returns the SOAP message given an endpoint, action, body of the request and the response object to marshal into.
func (c *Client) SOAPCall(ctx context.Context, endpoint, action string, headers http.Header, qv url.Values, body string, resp interface{}) error {
if body == "" {
return fmt.Errorf("cannot make a SOAP call with body set to empty string")
}
if err := c.checkResp(reflect.ValueOf(resp)); err != nil {
return err
}
if qv == nil {
qv = url.Values{}
}
u, err := url.Parse(endpoint)
if err != nil {
return fmt.Errorf("could not parse path URL(%s): %w", endpoint, err)
}
u.RawQuery = qv.Encode()
headers.Set("Content-Type", "application/soap+xml; charset=utf-8")
headers.Set("SOAPAction", action)
addStdHeaders(headers)
return c.xmlCall(ctx, u, headers, body, resp)
}
// xmlCall sends an XML in body and decodes into resp. This simply does the transport and relies on
// an upper level call to set things such as SOAP parameters and Content-Type, if required.
func (c *Client) xmlCall(ctx context.Context, u *url.URL, headers http.Header, body string, resp interface{}) error {
req := &http.Request{Method: http.MethodGet, URL: u, Header: headers}
if len(body) > 0 {
req.Method = http.MethodPost
req.Body = io.NopCloser(strings.NewReader(body))
}
data, err := c.do(ctx, req)
if err != nil {
return err
}
return xml.Unmarshal(data, resp)
}
// URLFormCall is used to make a call where we need to send application/x-www-form-urlencoded data
// to the backend and receive JSON back. qv will be encoded into the request body.
func (c *Client) URLFormCall(ctx context.Context, endpoint string, qv url.Values, resp interface{}) error {
if len(qv) == 0 {
return fmt.Errorf("URLFormCall() requires qv to have non-zero length")
}
if err := c.checkResp(reflect.ValueOf(resp)); err != nil {
return err
}
u, err := url.Parse(endpoint)
if err != nil {
return fmt.Errorf("could not parse path URL(%s): %w", endpoint, err)
}
headers := http.Header{}
headers.Set("Content-Type", "application/x-www-form-urlencoded; charset=utf-8")
addStdHeaders(headers)
enc := qv.Encode()
req := &http.Request{
Method: http.MethodPost,
URL: u,
Header: headers,
ContentLength: int64(len(enc)),
Body: io.NopCloser(strings.NewReader(enc)),
GetBody: func() (io.ReadCloser, error) {
return io.NopCloser(strings.NewReader(enc)), nil
},
}
data, err := c.do(ctx, req)
if err != nil {
return err
}
v := reflect.ValueOf(resp)
if err := c.checkResp(v); err != nil {
return err
}
var unmarshal = json.Unmarshal
if _, ok := v.Elem().Type().FieldByName("AdditionalFields"); ok {
unmarshal = customJSON.Unmarshal
}
if resp != nil {
if err := unmarshal(data, resp); err != nil {
return fmt.Errorf("json decode error: %w\nraw message was: %s", err, string(data))
}
}
return nil
}
// do makes the HTTP call to the server and returns the contents of the body.
func (c *Client) do(ctx context.Context, req *http.Request) ([]byte, error) {
if _, ok := ctx.Deadline(); !ok {
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, 30*time.Second)
defer cancel()
}
req = req.WithContext(ctx)
reply, err := c.client.Do(req)
if err != nil {
return nil, fmt.Errorf("server response error:\n %w", err)
}
defer reply.Body.Close()
data, err := c.readBody(reply)
if err != nil {
return nil, fmt.Errorf("could not read the body of an HTTP Response: %w", err)
}
reply.Body = io.NopCloser(bytes.NewBuffer(data))
// NOTE: This doesn't happen immediately after the call so that we can get an error message
// from the server and include it in our error.
switch reply.StatusCode {
case 200, 201:
default:
sd := strings.TrimSpace(string(data))
if sd != "" {
// We probably have the error in the body.
return nil, errors.CallErr{
Req: req,
Resp: reply,
Err: fmt.Errorf("http call(%s)(%s) error: reply status code was %d:\n%s", req.URL.String(), req.Method, reply.StatusCode, sd),
}
}
return nil, errors.CallErr{
Req: req,
Resp: reply,
Err: fmt.Errorf("http call(%s)(%s) error: reply status code was %d", req.URL.String(), req.Method, reply.StatusCode),
}
}
return data, nil
}
// checkResp checks a response object o make sure it is a pointer to a struct.
func (c *Client) checkResp(v reflect.Value) error {
if v.Kind() != reflect.Ptr {
return fmt.Errorf("bug: resp argument must a *struct, was %T", v.Interface())
}
v = v.Elem()
if v.Kind() != reflect.Struct {
return fmt.Errorf("bug: resp argument must be a *struct, was %T", v.Interface())
}
return nil
}
// readBody reads the body out of an *http.Response. It supports gzip encoded responses.
func (c *Client) readBody(resp *http.Response) ([]byte, error) {
var reader io.Reader = resp.Body
switch resp.Header.Get("Content-Encoding") {
case "":
// Do nothing
case "gzip":
reader = gzipDecompress(resp.Body)
default:
return nil, fmt.Errorf("bug: comm.Client.JSONCall(): content was send with unsupported content-encoding %s", resp.Header.Get("Content-Encoding"))
}
return io.ReadAll(reader)
}
var testID string
// addStdHeaders adds the standard headers we use on all calls.
func addStdHeaders(headers http.Header) http.Header {
headers.Set("Accept-Encoding", "gzip")
// So that I can have a static id for tests.
if testID != "" {
headers.Set("client-request-id", testID)
headers.Set("Return-Client-Request-Id", "false")
} else {
headers.Set("client-request-id", uuid.New().String())
headers.Set("Return-Client-Request-Id", "false")
}
headers.Set("x-client-sku", "MSAL.Go")
headers.Set("x-client-os", runtime.GOOS)
headers.Set("x-client-cpu", runtime.GOARCH)
headers.Set("x-client-ver", version.Version)
return headers
}

View file

@ -0,0 +1,33 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
package comm
import (
"compress/gzip"
"io"
)
func gzipDecompress(r io.Reader) io.Reader {
gzipReader, _ := gzip.NewReader(r)
pipeOut, pipeIn := io.Pipe()
go func() {
// decompression bomb would have to come from Azure services.
// If we want to limit, we should do that in comm.do().
_, err := io.Copy(pipeIn, gzipReader) //nolint
if err != nil {
// don't need the error.
pipeIn.CloseWithError(err) //nolint
gzipReader.Close()
return
}
if err := gzipReader.Close(); err != nil {
// don't need the error.
pipeIn.CloseWithError(err) //nolint
return
}
pipeIn.Close()
}()
return pipeOut
}

View file

@ -0,0 +1,17 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
// Package grant holds types of grants issued by authorization services.
package grant
const (
Password = "password"
JWT = "urn:ietf:params:oauth:grant-type:jwt-bearer"
SAMLV1 = "urn:ietf:params:oauth:grant-type:saml1_1-bearer"
SAMLV2 = "urn:ietf:params:oauth:grant-type:saml2-bearer"
DeviceCode = "device_code"
AuthCode = "authorization_code"
RefreshToken = "refresh_token"
ClientCredential = "client_credentials"
ClientAssertion = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"
)

View file

@ -0,0 +1,56 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
/*
Package ops provides operations to various backend services using REST clients.
The REST type provides several clients that can be used to communicate to backends.
Usage is simple:
rest := ops.New()
// Creates an authority client and calls the UserRealm() method.
userRealm, err := rest.Authority().UserRealm(ctx, authParameters)
if err != nil {
// Do something
}
*/
package ops
import (
"github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/oauth/ops/accesstokens"
"github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/oauth/ops/authority"
"github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/oauth/ops/internal/comm"
"github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/oauth/ops/wstrust"
)
// HTTPClient represents an HTTP client.
// It's usually an *http.Client from the standard library.
type HTTPClient = comm.HTTPClient
// REST provides REST clients for communicating with various backends used by MSAL.
type REST struct {
client *comm.Client
}
// New is the constructor for REST.
func New(httpClient HTTPClient) *REST {
return &REST{client: comm.New(httpClient)}
}
// Authority returns a client for querying information about various authorities.
func (r *REST) Authority() authority.Client {
return authority.Client{Comm: r.client}
}
// AccessTokens returns a client that can be used to get various access tokens for
// authorization purposes.
func (r *REST) AccessTokens() accesstokens.Client {
return accesstokens.Client{Comm: r.client}
}
// WSTrust provides access to various metadata in a WSTrust service. This data can
// be used to gain tokens based on SAML data using the client provided by AccessTokens().
func (r *REST) WSTrust() wstrust.Client {
return wstrust.Client{Comm: r.client}
}

View file

@ -0,0 +1,25 @@
// Code generated by "stringer -type=endpointType"; DO NOT EDIT.
package defs
import "strconv"
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
var x [1]struct{}
_ = x[etUnknown-0]
_ = x[etUsernamePassword-1]
_ = x[etWindowsTransport-2]
}
const _endpointType_name = "etUnknownetUsernamePasswordetWindowsTransport"
var _endpointType_index = [...]uint8{0, 9, 27, 45}
func (i endpointType) String() string {
if i < 0 || i >= endpointType(len(_endpointType_index)-1) {
return "endpointType(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _endpointType_name[_endpointType_index[i]:_endpointType_index[i+1]]
}

View file

@ -0,0 +1,394 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
package defs
import "encoding/xml"
type Definitions struct {
XMLName xml.Name `xml:"definitions"`
Text string `xml:",chardata"`
Name string `xml:"name,attr"`
TargetNamespace string `xml:"targetNamespace,attr"`
WSDL string `xml:"wsdl,attr"`
XSD string `xml:"xsd,attr"`
T string `xml:"t,attr"`
SOAPENC string `xml:"soapenc,attr"`
SOAP string `xml:"soap,attr"`
TNS string `xml:"tns,attr"`
MSC string `xml:"msc,attr"`
WSAM string `xml:"wsam,attr"`
SOAP12 string `xml:"soap12,attr"`
WSA10 string `xml:"wsa10,attr"`
WSA string `xml:"wsa,attr"`
WSAW string `xml:"wsaw,attr"`
WSX string `xml:"wsx,attr"`
WSAP string `xml:"wsap,attr"`
WSU string `xml:"wsu,attr"`
Trust string `xml:"trust,attr"`
WSP string `xml:"wsp,attr"`
Policy []Policy `xml:"Policy"`
Types Types `xml:"types"`
Message []Message `xml:"message"`
PortType []PortType `xml:"portType"`
Binding []Binding `xml:"binding"`
Service Service `xml:"service"`
}
type Policy struct {
Text string `xml:",chardata"`
ID string `xml:"Id,attr"`
ExactlyOne ExactlyOne `xml:"ExactlyOne"`
}
type ExactlyOne struct {
Text string `xml:",chardata"`
All All `xml:"All"`
}
type All struct {
Text string `xml:",chardata"`
NegotiateAuthentication NegotiateAuthentication `xml:"NegotiateAuthentication"`
TransportBinding TransportBinding `xml:"TransportBinding"`
UsingAddressing Text `xml:"UsingAddressing"`
EndorsingSupportingTokens EndorsingSupportingTokens `xml:"EndorsingSupportingTokens"`
WSS11 WSS11 `xml:"Wss11"`
Trust10 Trust10 `xml:"Trust10"`
SignedSupportingTokens SignedSupportingTokens `xml:"SignedSupportingTokens"`
Trust13 WSTrust13 `xml:"Trust13"`
SignedEncryptedSupportingTokens SignedEncryptedSupportingTokens `xml:"SignedEncryptedSupportingTokens"`
}
type NegotiateAuthentication struct {
Text string `xml:",chardata"`
HTTP string `xml:"http,attr"`
XMLName xml.Name
}
type TransportBinding struct {
Text string `xml:",chardata"`
SP string `xml:"sp,attr"`
Policy TransportBindingPolicy `xml:"Policy"`
}
type TransportBindingPolicy struct {
Text string `xml:",chardata"`
TransportToken TransportToken `xml:"TransportToken"`
AlgorithmSuite AlgorithmSuite `xml:"AlgorithmSuite"`
Layout Layout `xml:"Layout"`
IncludeTimestamp Text `xml:"IncludeTimestamp"`
}
type TransportToken struct {
Text string `xml:",chardata"`
Policy TransportTokenPolicy `xml:"Policy"`
}
type TransportTokenPolicy struct {
Text string `xml:",chardata"`
HTTPSToken HTTPSToken `xml:"HttpsToken"`
}
type HTTPSToken struct {
Text string `xml:",chardata"`
RequireClientCertificate string `xml:"RequireClientCertificate,attr"`
}
type AlgorithmSuite struct {
Text string `xml:",chardata"`
Policy AlgorithmSuitePolicy `xml:"Policy"`
}
type AlgorithmSuitePolicy struct {
Text string `xml:",chardata"`
Basic256 Text `xml:"Basic256"`
Basic128 Text `xml:"Basic128"`
}
type Layout struct {
Text string `xml:",chardata"`
Policy LayoutPolicy `xml:"Policy"`
}
type LayoutPolicy struct {
Text string `xml:",chardata"`
Strict Text `xml:"Strict"`
}
type EndorsingSupportingTokens struct {
Text string `xml:",chardata"`
SP string `xml:"sp,attr"`
Policy EndorsingSupportingTokensPolicy `xml:"Policy"`
}
type EndorsingSupportingTokensPolicy struct {
Text string `xml:",chardata"`
X509Token X509Token `xml:"X509Token"`
RSAToken RSAToken `xml:"RsaToken"`
SignedParts SignedParts `xml:"SignedParts"`
KerberosToken KerberosToken `xml:"KerberosToken"`
IssuedToken IssuedToken `xml:"IssuedToken"`
KeyValueToken KeyValueToken `xml:"KeyValueToken"`
}
type X509Token struct {
Text string `xml:",chardata"`
IncludeToken string `xml:"IncludeToken,attr"`
Policy X509TokenPolicy `xml:"Policy"`
}
type X509TokenPolicy struct {
Text string `xml:",chardata"`
RequireThumbprintReference Text `xml:"RequireThumbprintReference"`
WSSX509V3Token10 Text `xml:"WssX509V3Token10"`
}
type RSAToken struct {
Text string `xml:",chardata"`
IncludeToken string `xml:"IncludeToken,attr"`
Optional string `xml:"Optional,attr"`
MSSP string `xml:"mssp,attr"`
}
type SignedParts struct {
Text string `xml:",chardata"`
Header SignedPartsHeader `xml:"Header"`
}
type SignedPartsHeader struct {
Text string `xml:",chardata"`
Name string `xml:"Name,attr"`
Namespace string `xml:"Namespace,attr"`
}
type KerberosToken struct {
Text string `xml:",chardata"`
IncludeToken string `xml:"IncludeToken,attr"`
Policy KerberosTokenPolicy `xml:"Policy"`
}
type KerberosTokenPolicy struct {
Text string `xml:",chardata"`
WSSGSSKerberosV5ApReqToken11 Text `xml:"WssGssKerberosV5ApReqToken11"`
}
type IssuedToken struct {
Text string `xml:",chardata"`
IncludeToken string `xml:"IncludeToken,attr"`
RequestSecurityTokenTemplate RequestSecurityTokenTemplate `xml:"RequestSecurityTokenTemplate"`
Policy IssuedTokenPolicy `xml:"Policy"`
}
type RequestSecurityTokenTemplate struct {
Text string `xml:",chardata"`
KeyType Text `xml:"KeyType"`
EncryptWith Text `xml:"EncryptWith"`
SignatureAlgorithm Text `xml:"SignatureAlgorithm"`
CanonicalizationAlgorithm Text `xml:"CanonicalizationAlgorithm"`
EncryptionAlgorithm Text `xml:"EncryptionAlgorithm"`
KeySize Text `xml:"KeySize"`
KeyWrapAlgorithm Text `xml:"KeyWrapAlgorithm"`
}
type IssuedTokenPolicy struct {
Text string `xml:",chardata"`
RequireInternalReference Text `xml:"RequireInternalReference"`
}
type KeyValueToken struct {
Text string `xml:",chardata"`
IncludeToken string `xml:"IncludeToken,attr"`
Optional string `xml:"Optional,attr"`
}
type WSS11 struct {
Text string `xml:",chardata"`
SP string `xml:"sp,attr"`
Policy Wss11Policy `xml:"Policy"`
}
type Wss11Policy struct {
Text string `xml:",chardata"`
MustSupportRefThumbprint Text `xml:"MustSupportRefThumbprint"`
}
type Trust10 struct {
Text string `xml:",chardata"`
SP string `xml:"sp,attr"`
Policy Trust10Policy `xml:"Policy"`
}
type Trust10Policy struct {
Text string `xml:",chardata"`
MustSupportIssuedTokens Text `xml:"MustSupportIssuedTokens"`
RequireClientEntropy Text `xml:"RequireClientEntropy"`
RequireServerEntropy Text `xml:"RequireServerEntropy"`
}
type SignedSupportingTokens struct {
Text string `xml:",chardata"`
SP string `xml:"sp,attr"`
Policy SupportingTokensPolicy `xml:"Policy"`
}
type SupportingTokensPolicy struct {
Text string `xml:",chardata"`
UsernameToken UsernameToken `xml:"UsernameToken"`
}
type UsernameToken struct {
Text string `xml:",chardata"`
IncludeToken string `xml:"IncludeToken,attr"`
Policy UsernameTokenPolicy `xml:"Policy"`
}
type UsernameTokenPolicy struct {
Text string `xml:",chardata"`
WSSUsernameToken10 WSSUsernameToken10 `xml:"WssUsernameToken10"`
}
type WSSUsernameToken10 struct {
Text string `xml:",chardata"`
XMLName xml.Name
}
type WSTrust13 struct {
Text string `xml:",chardata"`
SP string `xml:"sp,attr"`
Policy WSTrust13Policy `xml:"Policy"`
}
type WSTrust13Policy struct {
Text string `xml:",chardata"`
MustSupportIssuedTokens Text `xml:"MustSupportIssuedTokens"`
RequireClientEntropy Text `xml:"RequireClientEntropy"`
RequireServerEntropy Text `xml:"RequireServerEntropy"`
}
type SignedEncryptedSupportingTokens struct {
Text string `xml:",chardata"`
SP string `xml:"sp,attr"`
Policy SupportingTokensPolicy `xml:"Policy"`
}
type Types struct {
Text string `xml:",chardata"`
Schema Schema `xml:"schema"`
}
type Schema struct {
Text string `xml:",chardata"`
TargetNamespace string `xml:"targetNamespace,attr"`
Import []Import `xml:"import"`
}
type Import struct {
Text string `xml:",chardata"`
SchemaLocation string `xml:"schemaLocation,attr"`
Namespace string `xml:"namespace,attr"`
}
type Message struct {
Text string `xml:",chardata"`
Name string `xml:"name,attr"`
Part Part `xml:"part"`
}
type Part struct {
Text string `xml:",chardata"`
Name string `xml:"name,attr"`
Element string `xml:"element,attr"`
}
type PortType struct {
Text string `xml:",chardata"`
Name string `xml:"name,attr"`
Operation Operation `xml:"operation"`
}
type Operation struct {
Text string `xml:",chardata"`
Name string `xml:"name,attr"`
Input OperationIO `xml:"input"`
Output OperationIO `xml:"output"`
}
type OperationIO struct {
Text string `xml:",chardata"`
Action string `xml:"Action,attr"`
Message string `xml:"message,attr"`
Body OperationIOBody `xml:"body"`
}
type OperationIOBody struct {
Text string `xml:",chardata"`
Use string `xml:"use,attr"`
}
type Binding struct {
Text string `xml:",chardata"`
Name string `xml:"name,attr"`
Type string `xml:"type,attr"`
PolicyReference PolicyReference `xml:"PolicyReference"`
Binding DefinitionsBinding `xml:"binding"`
Operation BindingOperation `xml:"operation"`
}
type PolicyReference struct {
Text string `xml:",chardata"`
URI string `xml:"URI,attr"`
}
type DefinitionsBinding struct {
Text string `xml:",chardata"`
Transport string `xml:"transport,attr"`
}
type BindingOperation struct {
Text string `xml:",chardata"`
Name string `xml:"name,attr"`
Operation BindingOperationOperation `xml:"operation"`
Input BindingOperationIO `xml:"input"`
Output BindingOperationIO `xml:"output"`
}
type BindingOperationOperation struct {
Text string `xml:",chardata"`
SoapAction string `xml:"soapAction,attr"`
Style string `xml:"style,attr"`
}
type BindingOperationIO struct {
Text string `xml:",chardata"`
Body OperationIOBody `xml:"body"`
}
type Service struct {
Text string `xml:",chardata"`
Name string `xml:"name,attr"`
Port []Port `xml:"port"`
}
type Port struct {
Text string `xml:",chardata"`
Name string `xml:"name,attr"`
Binding string `xml:"binding,attr"`
Address Address `xml:"address"`
EndpointReference PortEndpointReference `xml:"EndpointReference"`
}
type Address struct {
Text string `xml:",chardata"`
Location string `xml:"location,attr"`
}
type PortEndpointReference struct {
Text string `xml:",chardata"`
Address Text `xml:"Address"`
Identity Identity `xml:"Identity"`
}
type Identity struct {
Text string `xml:",chardata"`
XMLNS string `xml:"xmlns,attr"`
SPN Text `xml:"Spn"`
}

View file

@ -0,0 +1,230 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
package defs
import "encoding/xml"
// TODO(msal): Someone (and it ain't gonna be me) needs to document these attributes or
// at the least put a link to RFC.
type SAMLDefinitions struct {
XMLName xml.Name `xml:"Envelope"`
Text string `xml:",chardata"`
S string `xml:"s,attr"`
A string `xml:"a,attr"`
U string `xml:"u,attr"`
Header Header `xml:"Header"`
Body Body `xml:"Body"`
}
type Header struct {
Text string `xml:",chardata"`
Action Action `xml:"Action"`
Security Security `xml:"Security"`
}
type Action struct {
Text string `xml:",chardata"`
MustUnderstand string `xml:"mustUnderstand,attr"`
}
type Security struct {
Text string `xml:",chardata"`
MustUnderstand string `xml:"mustUnderstand,attr"`
O string `xml:"o,attr"`
Timestamp Timestamp `xml:"Timestamp"`
}
type Timestamp struct {
Text string `xml:",chardata"`
ID string `xml:"Id,attr"`
Created Text `xml:"Created"`
Expires Text `xml:"Expires"`
}
type Text struct {
Text string `xml:",chardata"`
}
type Body struct {
Text string `xml:",chardata"`
RequestSecurityTokenResponseCollection RequestSecurityTokenResponseCollection `xml:"RequestSecurityTokenResponseCollection"`
}
type RequestSecurityTokenResponseCollection struct {
Text string `xml:",chardata"`
Trust string `xml:"trust,attr"`
RequestSecurityTokenResponse []RequestSecurityTokenResponse `xml:"RequestSecurityTokenResponse"`
}
type RequestSecurityTokenResponse struct {
Text string `xml:",chardata"`
Lifetime Lifetime `xml:"Lifetime"`
AppliesTo AppliesTo `xml:"AppliesTo"`
RequestedSecurityToken RequestedSecurityToken `xml:"RequestedSecurityToken"`
RequestedAttachedReference RequestedAttachedReference `xml:"RequestedAttachedReference"`
RequestedUnattachedReference RequestedUnattachedReference `xml:"RequestedUnattachedReference"`
TokenType Text `xml:"TokenType"`
RequestType Text `xml:"RequestType"`
KeyType Text `xml:"KeyType"`
}
type Lifetime struct {
Text string `xml:",chardata"`
Created WSUTimestamp `xml:"Created"`
Expires WSUTimestamp `xml:"Expires"`
}
type WSUTimestamp struct {
Text string `xml:",chardata"`
Wsu string `xml:"wsu,attr"`
}
type AppliesTo struct {
Text string `xml:",chardata"`
Wsp string `xml:"wsp,attr"`
EndpointReference EndpointReference `xml:"EndpointReference"`
}
type EndpointReference struct {
Text string `xml:",chardata"`
Wsa string `xml:"wsa,attr"`
Address Text `xml:"Address"`
}
type RequestedSecurityToken struct {
Text string `xml:",chardata"`
AssertionRawXML string `xml:",innerxml"`
Assertion Assertion `xml:"Assertion"`
}
type Assertion struct {
XMLName xml.Name // Normally its `xml:"Assertion"`, but I think they want to capture the xmlns
Text string `xml:",chardata"`
MajorVersion string `xml:"MajorVersion,attr"`
MinorVersion string `xml:"MinorVersion,attr"`
AssertionID string `xml:"AssertionID,attr"`
Issuer string `xml:"Issuer,attr"`
IssueInstant string `xml:"IssueInstant,attr"`
Saml string `xml:"saml,attr"`
Conditions Conditions `xml:"Conditions"`
AttributeStatement AttributeStatement `xml:"AttributeStatement"`
AuthenticationStatement AuthenticationStatement `xml:"AuthenticationStatement"`
Signature Signature `xml:"Signature"`
}
type Conditions struct {
Text string `xml:",chardata"`
NotBefore string `xml:"NotBefore,attr"`
NotOnOrAfter string `xml:"NotOnOrAfter,attr"`
AudienceRestrictionCondition AudienceRestrictionCondition `xml:"AudienceRestrictionCondition"`
}
type AudienceRestrictionCondition struct {
Text string `xml:",chardata"`
Audience Text `xml:"Audience"`
}
type AttributeStatement struct {
Text string `xml:",chardata"`
Subject Subject `xml:"Subject"`
Attribute []Attribute `xml:"Attribute"`
}
type Subject struct {
Text string `xml:",chardata"`
NameIdentifier NameIdentifier `xml:"NameIdentifier"`
SubjectConfirmation SubjectConfirmation `xml:"SubjectConfirmation"`
}
type NameIdentifier struct {
Text string `xml:",chardata"`
Format string `xml:"Format,attr"`
}
type SubjectConfirmation struct {
Text string `xml:",chardata"`
ConfirmationMethod Text `xml:"ConfirmationMethod"`
}
type Attribute struct {
Text string `xml:",chardata"`
AttributeName string `xml:"AttributeName,attr"`
AttributeNamespace string `xml:"AttributeNamespace,attr"`
AttributeValue Text `xml:"AttributeValue"`
}
type AuthenticationStatement struct {
Text string `xml:",chardata"`
AuthenticationMethod string `xml:"AuthenticationMethod,attr"`
AuthenticationInstant string `xml:"AuthenticationInstant,attr"`
Subject Subject `xml:"Subject"`
}
type Signature struct {
Text string `xml:",chardata"`
Ds string `xml:"ds,attr"`
SignedInfo SignedInfo `xml:"SignedInfo"`
SignatureValue Text `xml:"SignatureValue"`
KeyInfo KeyInfo `xml:"KeyInfo"`
}
type SignedInfo struct {
Text string `xml:",chardata"`
CanonicalizationMethod Method `xml:"CanonicalizationMethod"`
SignatureMethod Method `xml:"SignatureMethod"`
Reference Reference `xml:"Reference"`
}
type Method struct {
Text string `xml:",chardata"`
Algorithm string `xml:"Algorithm,attr"`
}
type Reference struct {
Text string `xml:",chardata"`
URI string `xml:"URI,attr"`
Transforms Transforms `xml:"Transforms"`
DigestMethod Method `xml:"DigestMethod"`
DigestValue Text `xml:"DigestValue"`
}
type Transforms struct {
Text string `xml:",chardata"`
Transform []Method `xml:"Transform"`
}
type KeyInfo struct {
Text string `xml:",chardata"`
Xmlns string `xml:"xmlns,attr"`
X509Data X509Data `xml:"X509Data"`
}
type X509Data struct {
Text string `xml:",chardata"`
X509Certificate Text `xml:"X509Certificate"`
}
type RequestedAttachedReference struct {
Text string `xml:",chardata"`
SecurityTokenReference SecurityTokenReference `xml:"SecurityTokenReference"`
}
type SecurityTokenReference struct {
Text string `xml:",chardata"`
TokenType string `xml:"TokenType,attr"`
O string `xml:"o,attr"`
K string `xml:"k,attr"`
KeyIdentifier KeyIdentifier `xml:"KeyIdentifier"`
}
type KeyIdentifier struct {
Text string `xml:",chardata"`
ValueType string `xml:"ValueType,attr"`
}
type RequestedUnattachedReference struct {
Text string `xml:",chardata"`
SecurityTokenReference SecurityTokenReference `xml:"SecurityTokenReference"`
}

View file

@ -0,0 +1,25 @@
// Code generated by "stringer -type=Version"; DO NOT EDIT.
package defs
import "strconv"
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
var x [1]struct{}
_ = x[TrustUnknown-0]
_ = x[Trust2005-1]
_ = x[Trust13-2]
}
const _Version_name = "TrustUnknownTrust2005Trust13"
var _Version_index = [...]uint8{0, 12, 21, 28}
func (i Version) String() string {
if i < 0 || i >= Version(len(_Version_index)-1) {
return "Version(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _Version_name[_Version_index[i]:_Version_index[i+1]]
}

View file

@ -0,0 +1,199 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
package defs
import (
"encoding/xml"
"fmt"
"time"
"github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/oauth/ops/authority"
uuid "github.com/google/uuid"
)
//go:generate stringer -type=Version
type Version int
const (
TrustUnknown Version = iota
Trust2005
Trust13
)
// Endpoint represents a WSTrust endpoint.
type Endpoint struct {
// Version is the version of the endpoint.
Version Version
// URL is the URL of the endpoint.
URL string
}
type wsTrustTokenRequestEnvelope struct {
XMLName xml.Name `xml:"s:Envelope"`
Text string `xml:",chardata"`
S string `xml:"xmlns:s,attr"`
Wsa string `xml:"xmlns:wsa,attr"`
Wsu string `xml:"xmlns:wsu,attr"`
Header struct {
Text string `xml:",chardata"`
Action struct {
Text string `xml:",chardata"`
MustUnderstand string `xml:"s:mustUnderstand,attr"`
} `xml:"wsa:Action"`
MessageID struct {
Text string `xml:",chardata"`
} `xml:"wsa:messageID"`
ReplyTo struct {
Text string `xml:",chardata"`
Address struct {
Text string `xml:",chardata"`
} `xml:"wsa:Address"`
} `xml:"wsa:ReplyTo"`
To struct {
Text string `xml:",chardata"`
MustUnderstand string `xml:"s:mustUnderstand,attr"`
} `xml:"wsa:To"`
Security struct {
Text string `xml:",chardata"`
MustUnderstand string `xml:"s:mustUnderstand,attr"`
Wsse string `xml:"xmlns:wsse,attr"`
Timestamp struct {
Text string `xml:",chardata"`
ID string `xml:"wsu:Id,attr"`
Created struct {
Text string `xml:",chardata"`
} `xml:"wsu:Created"`
Expires struct {
Text string `xml:",chardata"`
} `xml:"wsu:Expires"`
} `xml:"wsu:Timestamp"`
UsernameToken struct {
Text string `xml:",chardata"`
ID string `xml:"wsu:Id,attr"`
Username struct {
Text string `xml:",chardata"`
} `xml:"wsse:Username"`
Password struct {
Text string `xml:",chardata"`
} `xml:"wsse:Password"`
} `xml:"wsse:UsernameToken"`
} `xml:"wsse:Security"`
} `xml:"s:Header"`
Body struct {
Text string `xml:",chardata"`
RequestSecurityToken struct {
Text string `xml:",chardata"`
Wst string `xml:"xmlns:wst,attr"`
AppliesTo struct {
Text string `xml:",chardata"`
Wsp string `xml:"xmlns:wsp,attr"`
EndpointReference struct {
Text string `xml:",chardata"`
Address struct {
Text string `xml:",chardata"`
} `xml:"wsa:Address"`
} `xml:"wsa:EndpointReference"`
} `xml:"wsp:AppliesTo"`
KeyType struct {
Text string `xml:",chardata"`
} `xml:"wst:KeyType"`
RequestType struct {
Text string `xml:",chardata"`
} `xml:"wst:RequestType"`
} `xml:"wst:RequestSecurityToken"`
} `xml:"s:Body"`
}
func buildTimeString(t time.Time) string {
// Golang time formats are weird: https://stackoverflow.com/questions/20234104/how-to-format-current-time-using-a-yyyymmddhhmmss-format
return t.Format("2006-01-02T15:04:05.000Z")
}
func (wte *Endpoint) buildTokenRequestMessage(authType authority.AuthorizeType, cloudAudienceURN string, username string, password string) (string, error) {
var soapAction string
var trustNamespace string
var keyType string
var requestType string
createdTime := time.Now().UTC()
expiresTime := createdTime.Add(10 * time.Minute)
switch wte.Version {
case Trust2005:
soapAction = trust2005Spec
trustNamespace = "http://schemas.xmlsoap.org/ws/2005/02/trust"
keyType = "http://schemas.xmlsoap.org/ws/2005/05/identity/NoProofKey"
requestType = "http://schemas.xmlsoap.org/ws/2005/02/trust/Issue"
case Trust13:
soapAction = trust13Spec
trustNamespace = "http://docs.oasis-open.org/ws-sx/ws-trust/200512"
keyType = "http://docs.oasis-open.org/ws-sx/ws-trust/200512/Bearer"
requestType = "http://docs.oasis-open.org/ws-sx/ws-trust/200512/Issue"
default:
return "", fmt.Errorf("buildTokenRequestMessage had Version == %q, which is not recognized", wte.Version)
}
var envelope wsTrustTokenRequestEnvelope
messageUUID := uuid.New()
envelope.S = "http://www.w3.org/2003/05/soap-envelope"
envelope.Wsa = "http://www.w3.org/2005/08/addressing"
envelope.Wsu = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd"
envelope.Header.Action.MustUnderstand = "1"
envelope.Header.Action.Text = soapAction
envelope.Header.MessageID.Text = "urn:uuid:" + messageUUID.String()
envelope.Header.ReplyTo.Address.Text = "http://www.w3.org/2005/08/addressing/anonymous"
envelope.Header.To.MustUnderstand = "1"
envelope.Header.To.Text = wte.URL
switch authType {
case authority.ATUnknown:
return "", fmt.Errorf("buildTokenRequestMessage had no authority type(%v)", authType)
case authority.ATUsernamePassword:
endpointUUID := uuid.New()
var trustID string
if wte.Version == Trust2005 {
trustID = "UnPwSecTok2005-" + endpointUUID.String()
} else {
trustID = "UnPwSecTok13-" + endpointUUID.String()
}
envelope.Header.Security.MustUnderstand = "1"
envelope.Header.Security.Wsse = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"
envelope.Header.Security.Timestamp.ID = "MSATimeStamp"
envelope.Header.Security.Timestamp.Created.Text = buildTimeString(createdTime)
envelope.Header.Security.Timestamp.Expires.Text = buildTimeString(expiresTime)
envelope.Header.Security.UsernameToken.ID = trustID
envelope.Header.Security.UsernameToken.Username.Text = username
envelope.Header.Security.UsernameToken.Password.Text = password
default:
// This is just to note that we don't do anything for other cases.
// We aren't missing anything I know of.
}
envelope.Body.RequestSecurityToken.Wst = trustNamespace
envelope.Body.RequestSecurityToken.AppliesTo.Wsp = "http://schemas.xmlsoap.org/ws/2004/09/policy"
envelope.Body.RequestSecurityToken.AppliesTo.EndpointReference.Address.Text = cloudAudienceURN
envelope.Body.RequestSecurityToken.KeyType.Text = keyType
envelope.Body.RequestSecurityToken.RequestType.Text = requestType
output, err := xml.Marshal(envelope)
if err != nil {
return "", err
}
return string(output), nil
}
func (wte *Endpoint) BuildTokenRequestMessageWIA(cloudAudienceURN string) (string, error) {
return wte.buildTokenRequestMessage(authority.ATWindowsIntegrated, cloudAudienceURN, "", "")
}
func (wte *Endpoint) BuildTokenRequestMessageUsernamePassword(cloudAudienceURN string, username string, password string) (string, error) {
return wte.buildTokenRequestMessage(authority.ATUsernamePassword, cloudAudienceURN, username, password)
}

View file

@ -0,0 +1,159 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
package defs
import (
"errors"
"fmt"
"strings"
)
//go:generate stringer -type=endpointType
type endpointType int
const (
etUnknown endpointType = iota
etUsernamePassword
etWindowsTransport
)
type wsEndpointData struct {
Version Version
EndpointType endpointType
}
const trust13Spec string = "http://docs.oasis-open.org/ws-sx/ws-trust/200512/RST/Issue"
const trust2005Spec string = "http://schemas.xmlsoap.org/ws/2005/02/trust/RST/Issue"
type MexDocument struct {
UsernamePasswordEndpoint Endpoint
WindowsTransportEndpoint Endpoint
policies map[string]endpointType
bindings map[string]wsEndpointData
}
func updateEndpoint(cached *Endpoint, found Endpoint) {
if cached == nil || cached.Version == TrustUnknown {
*cached = found
return
}
if (*cached).Version == Trust2005 && found.Version == Trust13 {
*cached = found
return
}
}
// TODO(msal): Someone needs to write tests for everything below.
// NewFromDef creates a new MexDocument.
func NewFromDef(defs Definitions) (MexDocument, error) {
policies, err := policies(defs)
if err != nil {
return MexDocument{}, err
}
bindings, err := bindings(defs, policies)
if err != nil {
return MexDocument{}, err
}
userPass, windows, err := endpoints(defs, bindings)
if err != nil {
return MexDocument{}, err
}
return MexDocument{
UsernamePasswordEndpoint: userPass,
WindowsTransportEndpoint: windows,
policies: policies,
bindings: bindings,
}, nil
}
func policies(defs Definitions) (map[string]endpointType, error) {
policies := make(map[string]endpointType, len(defs.Policy))
for _, policy := range defs.Policy {
if policy.ExactlyOne.All.NegotiateAuthentication.XMLName.Local != "" {
if policy.ExactlyOne.All.TransportBinding.SP != "" && policy.ID != "" {
policies["#"+policy.ID] = etWindowsTransport
}
}
if policy.ExactlyOne.All.SignedEncryptedSupportingTokens.Policy.UsernameToken.Policy.WSSUsernameToken10.XMLName.Local != "" {
if policy.ExactlyOne.All.TransportBinding.SP != "" && policy.ID != "" {
policies["#"+policy.ID] = etUsernamePassword
}
}
if policy.ExactlyOne.All.SignedSupportingTokens.Policy.UsernameToken.Policy.WSSUsernameToken10.XMLName.Local != "" {
if policy.ExactlyOne.All.TransportBinding.SP != "" && policy.ID != "" {
policies["#"+policy.ID] = etUsernamePassword
}
}
}
if len(policies) == 0 {
return policies, errors.New("no policies for mex document")
}
return policies, nil
}
func bindings(defs Definitions, policies map[string]endpointType) (map[string]wsEndpointData, error) {
bindings := make(map[string]wsEndpointData, len(defs.Binding))
for _, binding := range defs.Binding {
policyName := binding.PolicyReference.URI
transport := binding.Binding.Transport
if transport == "http://schemas.xmlsoap.org/soap/http" {
if policy, ok := policies[policyName]; ok {
bindingName := binding.Name
specVersion := binding.Operation.Operation.SoapAction
if specVersion == trust13Spec {
bindings[bindingName] = wsEndpointData{Trust13, policy}
} else if specVersion == trust2005Spec {
bindings[bindingName] = wsEndpointData{Trust2005, policy}
} else {
return nil, errors.New("found unknown spec version in mex document")
}
}
}
}
return bindings, nil
}
func endpoints(defs Definitions, bindings map[string]wsEndpointData) (userPass, windows Endpoint, err error) {
for _, port := range defs.Service.Port {
bindingName := port.Binding
index := strings.Index(bindingName, ":")
if index != -1 {
bindingName = bindingName[index+1:]
}
if binding, ok := bindings[bindingName]; ok {
url := strings.TrimSpace(port.EndpointReference.Address.Text)
if url == "" {
return Endpoint{}, Endpoint{}, fmt.Errorf("MexDocument cannot have blank URL endpoint")
}
if binding.Version == TrustUnknown {
return Endpoint{}, Endpoint{}, fmt.Errorf("endpoint version unknown")
}
endpoint := Endpoint{Version: binding.Version, URL: url}
switch binding.EndpointType {
case etUsernamePassword:
updateEndpoint(&userPass, endpoint)
case etWindowsTransport:
updateEndpoint(&windows, endpoint)
default:
return Endpoint{}, Endpoint{}, errors.New("found unknown port type in MEX document")
}
}
}
return userPass, windows, nil
}

View file

@ -0,0 +1,136 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
/*
Package wstrust provides a client for communicating with a WSTrust (https://en.wikipedia.org/wiki/WS-Trust#:~:text=WS%2DTrust%20is%20a%20WS,in%20a%20secure%20message%20exchange.)
for the purposes of extracting metadata from the service. This data can be used to acquire
tokens using the accesstokens.Client.GetAccessTokenFromSamlGrant() call.
*/
package wstrust
import (
"context"
"errors"
"fmt"
"net/http"
"net/url"
"github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/oauth/ops/authority"
"github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/oauth/ops/internal/grant"
"github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/oauth/ops/wstrust/defs"
)
type xmlCaller interface {
XMLCall(ctx context.Context, endpoint string, headers http.Header, qv url.Values, resp interface{}) error
SOAPCall(ctx context.Context, endpoint, action string, headers http.Header, qv url.Values, body string, resp interface{}) error
}
type SamlTokenInfo struct {
AssertionType string // Should be either constants SAMLV1Grant or SAMLV2Grant.
Assertion string
}
// Client represents the REST calls to get tokens from token generator backends.
type Client struct {
// Comm provides the HTTP transport client.
Comm xmlCaller
}
// TODO(msal): This allows me to call Mex without having a real Def file on line 45.
// This would fail because policies() would not find a policy. This is easy enough to
// fix in test data, but.... Definitions is defined with built in structs. That needs
// to be pulled apart and until then I have this hack in.
var newFromDef = defs.NewFromDef
// Mex provides metadata about a wstrust service.
func (c Client) Mex(ctx context.Context, federationMetadataURL string) (defs.MexDocument, error) {
resp := defs.Definitions{}
err := c.Comm.XMLCall(
ctx,
federationMetadataURL,
http.Header{},
nil,
&resp,
)
if err != nil {
return defs.MexDocument{}, err
}
return newFromDef(resp)
}
const (
SoapActionDefault = "http://docs.oasis-open.org/ws-sx/ws-trust/200512/RST/Issue"
// Note: Commented out because this action is not supported. It was in the original code
// but only used in a switch where it errored. Since there was only one value, a default
// worked better. However, buildTokenRequestMessage() had 2005 support. I'm not actually
// sure what's going on here. It like we have half support. For now this is here just
// for documentation purposes in case we are going to add support.
//
// SoapActionWSTrust2005 = "http://schemas.xmlsoap.org/ws/2005/02/trust/RST/Issue"
)
// SAMLTokenInfo provides SAML information that is used to generate a SAML token.
func (c Client) SAMLTokenInfo(ctx context.Context, authParameters authority.AuthParams, cloudAudienceURN string, endpoint defs.Endpoint) (SamlTokenInfo, error) {
var wsTrustRequestMessage string
var err error
switch authParameters.AuthorizationType {
case authority.ATWindowsIntegrated:
wsTrustRequestMessage, err = endpoint.BuildTokenRequestMessageWIA(cloudAudienceURN)
if err != nil {
return SamlTokenInfo{}, err
}
case authority.ATUsernamePassword:
wsTrustRequestMessage, err = endpoint.BuildTokenRequestMessageUsernamePassword(
cloudAudienceURN, authParameters.Username, authParameters.Password)
if err != nil {
return SamlTokenInfo{}, err
}
default:
return SamlTokenInfo{}, fmt.Errorf("unknown auth type %v", authParameters.AuthorizationType)
}
var soapAction string
switch endpoint.Version {
case defs.Trust13:
soapAction = SoapActionDefault
case defs.Trust2005:
return SamlTokenInfo{}, errors.New("WS Trust 2005 support is not implemented")
default:
return SamlTokenInfo{}, fmt.Errorf("the SOAP endpoint for a wstrust call had an invalid version: %v", endpoint.Version)
}
resp := defs.SAMLDefinitions{}
err = c.Comm.SOAPCall(ctx, endpoint.URL, soapAction, http.Header{}, nil, wsTrustRequestMessage, &resp)
if err != nil {
return SamlTokenInfo{}, err
}
return c.samlAssertion(resp)
}
const (
samlv1Assertion = "urn:oasis:names:tc:SAML:1.0:assertion"
samlv2Assertion = "urn:oasis:names:tc:SAML:2.0:assertion"
)
func (c Client) samlAssertion(def defs.SAMLDefinitions) (SamlTokenInfo, error) {
for _, tokenResponse := range def.Body.RequestSecurityTokenResponseCollection.RequestSecurityTokenResponse {
token := tokenResponse.RequestedSecurityToken
if token.Assertion.XMLName.Local != "" {
assertion := token.AssertionRawXML
samlVersion := token.Assertion.Saml
switch samlVersion {
case samlv1Assertion:
return SamlTokenInfo{AssertionType: grant.SAMLV1, Assertion: assertion}, nil
case samlv2Assertion:
return SamlTokenInfo{AssertionType: grant.SAMLV2, Assertion: assertion}, nil
}
return SamlTokenInfo{}, fmt.Errorf("couldn't parse SAML assertion, version unknown: %q", samlVersion)
}
}
return SamlTokenInfo{}, errors.New("unknown WS-Trust version")
}

View file

@ -0,0 +1,149 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
// TODO(msal): Write some tests. The original code this came from didn't have tests and I'm too
// tired at this point to do it. It, like many other *Manager code I found was broken because
// they didn't have mutex protection.
package oauth
import (
"context"
"errors"
"fmt"
"strings"
"sync"
"github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/oauth/ops"
"github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/oauth/ops/authority"
)
// ADFS is an active directory federation service authority type.
const ADFS = "ADFS"
type cacheEntry struct {
Endpoints authority.Endpoints
ValidForDomainsInList map[string]bool
}
func createcacheEntry(endpoints authority.Endpoints) cacheEntry {
return cacheEntry{endpoints, map[string]bool{}}
}
// AuthorityEndpoint retrieves endpoints from an authority for auth and token acquisition.
type authorityEndpoint struct {
rest *ops.REST
mu sync.Mutex
cache map[string]cacheEntry
}
// newAuthorityEndpoint is the constructor for AuthorityEndpoint.
func newAuthorityEndpoint(rest *ops.REST) *authorityEndpoint {
m := &authorityEndpoint{rest: rest, cache: map[string]cacheEntry{}}
return m
}
// ResolveEndpoints gets the authorization and token endpoints and creates an AuthorityEndpoints instance
func (m *authorityEndpoint) ResolveEndpoints(ctx context.Context, authorityInfo authority.Info, userPrincipalName string) (authority.Endpoints, error) {
if endpoints, found := m.cachedEndpoints(authorityInfo, userPrincipalName); found {
return endpoints, nil
}
endpoint, err := m.openIDConfigurationEndpoint(ctx, authorityInfo, userPrincipalName)
if err != nil {
return authority.Endpoints{}, err
}
resp, err := m.rest.Authority().GetTenantDiscoveryResponse(ctx, endpoint)
if err != nil {
return authority.Endpoints{}, err
}
if err := resp.Validate(); err != nil {
return authority.Endpoints{}, fmt.Errorf("ResolveEndpoints(): %w", err)
}
tenant := authorityInfo.Tenant
endpoints := authority.NewEndpoints(
strings.Replace(resp.AuthorizationEndpoint, "{tenant}", tenant, -1),
strings.Replace(resp.TokenEndpoint, "{tenant}", tenant, -1),
strings.Replace(resp.Issuer, "{tenant}", tenant, -1),
authorityInfo.Host)
m.addCachedEndpoints(authorityInfo, userPrincipalName, endpoints)
return endpoints, nil
}
// cachedEndpoints returns a the cached endpoints if they exists. If not, we return false.
func (m *authorityEndpoint) cachedEndpoints(authorityInfo authority.Info, userPrincipalName string) (authority.Endpoints, bool) {
m.mu.Lock()
defer m.mu.Unlock()
if cacheEntry, ok := m.cache[authorityInfo.CanonicalAuthorityURI]; ok {
if authorityInfo.AuthorityType == ADFS {
domain, err := adfsDomainFromUpn(userPrincipalName)
if err == nil {
if _, ok := cacheEntry.ValidForDomainsInList[domain]; ok {
return cacheEntry.Endpoints, true
}
}
}
return cacheEntry.Endpoints, true
}
return authority.Endpoints{}, false
}
func (m *authorityEndpoint) addCachedEndpoints(authorityInfo authority.Info, userPrincipalName string, endpoints authority.Endpoints) {
m.mu.Lock()
defer m.mu.Unlock()
updatedCacheEntry := createcacheEntry(endpoints)
if authorityInfo.AuthorityType == ADFS {
// Since we're here, we've made a call to the backend. We want to ensure we're caching
// the latest values from the server.
if cacheEntry, ok := m.cache[authorityInfo.CanonicalAuthorityURI]; ok {
for k := range cacheEntry.ValidForDomainsInList {
updatedCacheEntry.ValidForDomainsInList[k] = true
}
}
domain, err := adfsDomainFromUpn(userPrincipalName)
if err == nil {
updatedCacheEntry.ValidForDomainsInList[domain] = true
}
}
m.cache[authorityInfo.CanonicalAuthorityURI] = updatedCacheEntry
}
func (m *authorityEndpoint) openIDConfigurationEndpoint(ctx context.Context, authorityInfo authority.Info, userPrincipalName string) (string, error) {
if authorityInfo.Tenant == "adfs" {
return fmt.Sprintf("https://%s/adfs/.well-known/openid-configuration", authorityInfo.Host), nil
} else if authorityInfo.ValidateAuthority && !authority.TrustedHost(authorityInfo.Host) {
resp, err := m.rest.Authority().AADInstanceDiscovery(ctx, authorityInfo)
if err != nil {
return "", err
}
return resp.TenantDiscoveryEndpoint, nil
} else if authorityInfo.Region != "" {
resp, err := m.rest.Authority().AADInstanceDiscovery(ctx, authorityInfo)
if err != nil {
return "", err
}
return resp.TenantDiscoveryEndpoint, nil
}
return authorityInfo.CanonicalAuthorityURI + "v2.0/.well-known/openid-configuration", nil
}
func adfsDomainFromUpn(userPrincipalName string) (string, error) {
parts := strings.Split(userPrincipalName, "@")
if len(parts) < 2 {
return "", errors.New("no @ present in user principal name")
}
return parts[1], nil
}

View file

@ -0,0 +1,52 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
package options
import (
"errors"
"fmt"
)
// CallOption implements an optional argument to a method call. See
// https://blog.devgenius.io/go-call-option-that-can-be-used-with-multiple-methods-6c81734f3dbe
// for an explanation of the usage pattern.
type CallOption interface {
Do(any) error
callOption()
}
// ApplyOptions applies all the callOptions to options. options must be a pointer to a struct and
// callOptions must be a list of objects that implement CallOption.
func ApplyOptions[O, C any](options O, callOptions []C) error {
for _, o := range callOptions {
if t, ok := any(o).(CallOption); !ok {
return fmt.Errorf("unexpected option type %T", o)
} else if err := t.Do(options); err != nil {
return err
}
}
return nil
}
// NewCallOption returns a new CallOption whose Do() method calls function "f".
func NewCallOption(f func(any) error) CallOption {
if f == nil {
// This isn't a practical concern because only an MSAL maintainer can get
// us here, by implementing a do-nothing option. But if someone does that,
// the below ensures the method invoked with the option returns an error.
return callOption(func(any) error {
return errors.New("invalid option: missing implementation")
})
}
return callOption(f)
}
// callOption is an adapter for a function to a CallOption
type callOption func(any) error
func (c callOption) Do(a any) error {
return c(a)
}
func (callOption) callOption() {}

View file

@ -0,0 +1,71 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
package shared
import (
"net/http"
"reflect"
"strings"
)
const (
// CacheKeySeparator is used in creating the keys of the cache.
CacheKeySeparator = "-"
)
type Account struct {
HomeAccountID string `json:"home_account_id,omitempty"`
Environment string `json:"environment,omitempty"`
Realm string `json:"realm,omitempty"`
LocalAccountID string `json:"local_account_id,omitempty"`
AuthorityType string `json:"authority_type,omitempty"`
PreferredUsername string `json:"username,omitempty"`
GivenName string `json:"given_name,omitempty"`
FamilyName string `json:"family_name,omitempty"`
MiddleName string `json:"middle_name,omitempty"`
Name string `json:"name,omitempty"`
AlternativeID string `json:"alternative_account_id,omitempty"`
RawClientInfo string `json:"client_info,omitempty"`
UserAssertionHash string `json:"user_assertion_hash,omitempty"`
AdditionalFields map[string]interface{}
}
// NewAccount creates an account.
func NewAccount(homeAccountID, env, realm, localAccountID, authorityType, username string) Account {
return Account{
HomeAccountID: homeAccountID,
Environment: env,
Realm: realm,
LocalAccountID: localAccountID,
AuthorityType: authorityType,
PreferredUsername: username,
}
}
// Key creates the key for storing accounts in the cache.
func (acc Account) Key() string {
return strings.Join([]string{acc.HomeAccountID, acc.Environment, acc.Realm}, CacheKeySeparator)
}
// IsZero checks the zero value of account.
func (acc Account) IsZero() bool {
v := reflect.ValueOf(acc)
for i := 0; i < v.NumField(); i++ {
field := v.Field(i)
if !field.IsZero() {
switch field.Kind() {
case reflect.Map, reflect.Slice:
if field.Len() == 0 {
continue
}
}
return false
}
}
return true
}
// DefaultClient is our default shared HTTP client.
var DefaultClient = &http.Client{}

View file

@ -0,0 +1,8 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
// Package version keeps the version number of the client package.
package version
// Version is the version of this client package that is communicated to the server.
const Version = "1.0.0"

View file

@ -0,0 +1,683 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
/*
Package public provides a client for authentication of "public" applications. A "public"
application is defined as an app that runs on client devices (android, ios, windows, linux, ...).
These devices are "untrusted" and access resources via web APIs that must authenticate.
*/
package public
/*
Design note:
public.Client uses client.Base as an embedded type. client.Base statically assigns its attributes
during creation. As it doesn't have any pointers in it, anything borrowed from it, such as
Base.AuthParams is a copy that is free to be manipulated here.
*/
// TODO(msal): This should have example code for each method on client using Go's example doc framework.
// base usage details should be includee in the package documentation.
import (
"context"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"fmt"
"net/url"
"strconv"
"github.com/AzureAD/microsoft-authentication-library-for-go/apps/cache"
"github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/base"
"github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/local"
"github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/oauth"
"github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/oauth/ops"
"github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/oauth/ops/accesstokens"
"github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/oauth/ops/authority"
"github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/options"
"github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/shared"
"github.com/google/uuid"
"github.com/pkg/browser"
)
// AuthResult contains the results of one token acquisition operation.
// For details see https://aka.ms/msal-net-authenticationresult
type AuthResult = base.AuthResult
type Account = shared.Account
// clientOptions configures the Client's behavior.
type clientOptions struct {
accessor cache.ExportReplace
authority string
capabilities []string
disableInstanceDiscovery bool
httpClient ops.HTTPClient
}
func (p *clientOptions) validate() error {
u, err := url.Parse(p.authority)
if err != nil {
return fmt.Errorf("Authority options cannot be URL parsed: %w", err)
}
if u.Scheme != "https" {
return fmt.Errorf("Authority(%s) did not start with https://", u.String())
}
return nil
}
// Option is an optional argument to the New constructor.
type Option func(o *clientOptions)
// WithAuthority allows for a custom authority to be set. This must be a valid https url.
func WithAuthority(authority string) Option {
return func(o *clientOptions) {
o.authority = authority
}
}
// WithCache provides an accessor that will read and write authentication data to an externally managed cache.
func WithCache(accessor cache.ExportReplace) Option {
return func(o *clientOptions) {
o.accessor = accessor
}
}
// WithClientCapabilities allows configuring one or more client capabilities such as "CP1"
func WithClientCapabilities(capabilities []string) Option {
return func(o *clientOptions) {
// there's no danger of sharing the slice's underlying memory with the application because
// this slice is simply passed to base.WithClientCapabilities, which copies its data
o.capabilities = capabilities
}
}
// WithHTTPClient allows for a custom HTTP client to be set.
func WithHTTPClient(httpClient ops.HTTPClient) Option {
return func(o *clientOptions) {
o.httpClient = httpClient
}
}
// WithInstanceDiscovery set to false to disable authority validation (to support private cloud scenarios)
func WithInstanceDiscovery(enabled bool) Option {
return func(o *clientOptions) {
o.disableInstanceDiscovery = !enabled
}
}
// Client is a representation of authentication client for public applications as defined in the
// package doc. For more information, visit https://docs.microsoft.com/azure/active-directory/develop/msal-client-applications.
type Client struct {
base base.Client
}
// New is the constructor for Client.
func New(clientID string, options ...Option) (Client, error) {
opts := clientOptions{
authority: base.AuthorityPublicCloud,
httpClient: shared.DefaultClient,
}
for _, o := range options {
o(&opts)
}
if err := opts.validate(); err != nil {
return Client{}, err
}
base, err := base.New(clientID, opts.authority, oauth.New(opts.httpClient), base.WithCacheAccessor(opts.accessor), base.WithClientCapabilities(opts.capabilities), base.WithInstanceDiscovery(!opts.disableInstanceDiscovery))
if err != nil {
return Client{}, err
}
return Client{base}, nil
}
// authCodeURLOptions contains options for AuthCodeURL
type authCodeURLOptions struct {
claims, loginHint, tenantID, domainHint string
}
// AuthCodeURLOption is implemented by options for AuthCodeURL
type AuthCodeURLOption interface {
authCodeURLOption()
}
// AuthCodeURL creates a URL used to acquire an authorization code.
//
// Options: [WithClaims], [WithDomainHint], [WithLoginHint], [WithTenantID]
func (pca Client) AuthCodeURL(ctx context.Context, clientID, redirectURI string, scopes []string, opts ...AuthCodeURLOption) (string, error) {
o := authCodeURLOptions{}
if err := options.ApplyOptions(&o, opts); err != nil {
return "", err
}
ap, err := pca.base.AuthParams.WithTenant(o.tenantID)
if err != nil {
return "", err
}
ap.Claims = o.claims
ap.LoginHint = o.loginHint
ap.DomainHint = o.domainHint
return pca.base.AuthCodeURL(ctx, clientID, redirectURI, scopes, ap)
}
// WithClaims sets additional claims to request for the token, such as those required by conditional access policies.
// Use this option when Azure AD returned a claims challenge for a prior request. The argument must be decoded.
// This option is valid for any token acquisition method.
func WithClaims(claims string) interface {
AcquireByAuthCodeOption
AcquireByDeviceCodeOption
AcquireByUsernamePasswordOption
AcquireInteractiveOption
AcquireSilentOption
AuthCodeURLOption
options.CallOption
} {
return struct {
AcquireByAuthCodeOption
AcquireByDeviceCodeOption
AcquireByUsernamePasswordOption
AcquireInteractiveOption
AcquireSilentOption
AuthCodeURLOption
options.CallOption
}{
CallOption: options.NewCallOption(
func(a any) error {
switch t := a.(type) {
case *acquireTokenByAuthCodeOptions:
t.claims = claims
case *acquireTokenByDeviceCodeOptions:
t.claims = claims
case *acquireTokenByUsernamePasswordOptions:
t.claims = claims
case *acquireTokenSilentOptions:
t.claims = claims
case *authCodeURLOptions:
t.claims = claims
case *interactiveAuthOptions:
t.claims = claims
default:
return fmt.Errorf("unexpected options type %T", a)
}
return nil
},
),
}
}
// WithTenantID specifies a tenant for a single authentication. It may be different than the tenant set in [New] by [WithAuthority].
// This option is valid for any token acquisition method.
func WithTenantID(tenantID string) interface {
AcquireByAuthCodeOption
AcquireByDeviceCodeOption
AcquireByUsernamePasswordOption
AcquireInteractiveOption
AcquireSilentOption
AuthCodeURLOption
options.CallOption
} {
return struct {
AcquireByAuthCodeOption
AcquireByDeviceCodeOption
AcquireByUsernamePasswordOption
AcquireInteractiveOption
AcquireSilentOption
AuthCodeURLOption
options.CallOption
}{
CallOption: options.NewCallOption(
func(a any) error {
switch t := a.(type) {
case *acquireTokenByAuthCodeOptions:
t.tenantID = tenantID
case *acquireTokenByDeviceCodeOptions:
t.tenantID = tenantID
case *acquireTokenByUsernamePasswordOptions:
t.tenantID = tenantID
case *acquireTokenSilentOptions:
t.tenantID = tenantID
case *authCodeURLOptions:
t.tenantID = tenantID
case *interactiveAuthOptions:
t.tenantID = tenantID
default:
return fmt.Errorf("unexpected options type %T", a)
}
return nil
},
),
}
}
// acquireTokenSilentOptions are all the optional settings to an AcquireTokenSilent() call.
// These are set by using various AcquireTokenSilentOption functions.
type acquireTokenSilentOptions struct {
account Account
claims, tenantID string
}
// AcquireSilentOption is implemented by options for AcquireTokenSilent
type AcquireSilentOption interface {
acquireSilentOption()
}
// WithSilentAccount uses the passed account during an AcquireTokenSilent() call.
func WithSilentAccount(account Account) interface {
AcquireSilentOption
options.CallOption
} {
return struct {
AcquireSilentOption
options.CallOption
}{
CallOption: options.NewCallOption(
func(a any) error {
switch t := a.(type) {
case *acquireTokenSilentOptions:
t.account = account
default:
return fmt.Errorf("unexpected options type %T", a)
}
return nil
},
),
}
}
// AcquireTokenSilent acquires a token from either the cache or using a refresh token.
//
// Options: [WithClaims], [WithSilentAccount], [WithTenantID]
func (pca Client) AcquireTokenSilent(ctx context.Context, scopes []string, opts ...AcquireSilentOption) (AuthResult, error) {
o := acquireTokenSilentOptions{}
if err := options.ApplyOptions(&o, opts); err != nil {
return AuthResult{}, err
}
silentParameters := base.AcquireTokenSilentParameters{
Scopes: scopes,
Account: o.account,
Claims: o.claims,
RequestType: accesstokens.ATPublic,
IsAppCache: false,
TenantID: o.tenantID,
}
return pca.base.AcquireTokenSilent(ctx, silentParameters)
}
// acquireTokenByUsernamePasswordOptions contains optional configuration for AcquireTokenByUsernamePassword
type acquireTokenByUsernamePasswordOptions struct {
claims, tenantID string
}
// AcquireByUsernamePasswordOption is implemented by options for AcquireTokenByUsernamePassword
type AcquireByUsernamePasswordOption interface {
acquireByUsernamePasswordOption()
}
// AcquireTokenByUsernamePassword acquires a security token from the authority, via Username/Password Authentication.
// NOTE: this flow is NOT recommended.
//
// Options: [WithClaims], [WithTenantID]
func (pca Client) AcquireTokenByUsernamePassword(ctx context.Context, scopes []string, username, password string, opts ...AcquireByUsernamePasswordOption) (AuthResult, error) {
o := acquireTokenByUsernamePasswordOptions{}
if err := options.ApplyOptions(&o, opts); err != nil {
return AuthResult{}, err
}
authParams, err := pca.base.AuthParams.WithTenant(o.tenantID)
if err != nil {
return AuthResult{}, err
}
authParams.Scopes = scopes
authParams.AuthorizationType = authority.ATUsernamePassword
authParams.Claims = o.claims
authParams.Username = username
authParams.Password = password
token, err := pca.base.Token.UsernamePassword(ctx, authParams)
if err != nil {
return AuthResult{}, err
}
return pca.base.AuthResultFromToken(ctx, authParams, token, true)
}
type DeviceCodeResult = accesstokens.DeviceCodeResult
// DeviceCode provides the results of the device code flows first stage (containing the code)
// that must be entered on the second device and provides a method to retrieve the AuthenticationResult
// once that code has been entered and verified.
type DeviceCode struct {
// Result holds the information about the device code (such as the code).
Result DeviceCodeResult
authParams authority.AuthParams
client Client
dc oauth.DeviceCode
}
// AuthenticationResult retreives the AuthenticationResult once the user enters the code
// on the second device. Until then it blocks until the .AcquireTokenByDeviceCode() context
// is cancelled or the token expires.
func (d DeviceCode) AuthenticationResult(ctx context.Context) (AuthResult, error) {
token, err := d.dc.Token(ctx)
if err != nil {
return AuthResult{}, err
}
return d.client.base.AuthResultFromToken(ctx, d.authParams, token, true)
}
// acquireTokenByDeviceCodeOptions contains optional configuration for AcquireTokenByDeviceCode
type acquireTokenByDeviceCodeOptions struct {
claims, tenantID string
}
// AcquireByDeviceCodeOption is implemented by options for AcquireTokenByDeviceCode
type AcquireByDeviceCodeOption interface {
acquireByDeviceCodeOptions()
}
// AcquireTokenByDeviceCode acquires a security token from the authority, by acquiring a device code and using that to acquire the token.
// Users need to create an AcquireTokenDeviceCodeParameters instance and pass it in.
//
// Options: [WithClaims], [WithTenantID]
func (pca Client) AcquireTokenByDeviceCode(ctx context.Context, scopes []string, opts ...AcquireByDeviceCodeOption) (DeviceCode, error) {
o := acquireTokenByDeviceCodeOptions{}
if err := options.ApplyOptions(&o, opts); err != nil {
return DeviceCode{}, err
}
authParams, err := pca.base.AuthParams.WithTenant(o.tenantID)
if err != nil {
return DeviceCode{}, err
}
authParams.Scopes = scopes
authParams.AuthorizationType = authority.ATDeviceCode
authParams.Claims = o.claims
dc, err := pca.base.Token.DeviceCode(ctx, authParams)
if err != nil {
return DeviceCode{}, err
}
return DeviceCode{Result: dc.Result, authParams: authParams, client: pca, dc: dc}, nil
}
// acquireTokenByAuthCodeOptions contains the optional parameters used to acquire an access token using the authorization code flow.
type acquireTokenByAuthCodeOptions struct {
challenge, claims, tenantID string
}
// AcquireByAuthCodeOption is implemented by options for AcquireTokenByAuthCode
type AcquireByAuthCodeOption interface {
acquireByAuthCodeOption()
}
// WithChallenge allows you to provide a code for the .AcquireTokenByAuthCode() call.
func WithChallenge(challenge string) interface {
AcquireByAuthCodeOption
options.CallOption
} {
return struct {
AcquireByAuthCodeOption
options.CallOption
}{
CallOption: options.NewCallOption(
func(a any) error {
switch t := a.(type) {
case *acquireTokenByAuthCodeOptions:
t.challenge = challenge
default:
return fmt.Errorf("unexpected options type %T", a)
}
return nil
},
),
}
}
// AcquireTokenByAuthCode is a request to acquire a security token from the authority, using an authorization code.
// The specified redirect URI must be the same URI that was used when the authorization code was requested.
//
// Options: [WithChallenge], [WithClaims], [WithTenantID]
func (pca Client) AcquireTokenByAuthCode(ctx context.Context, code string, redirectURI string, scopes []string, opts ...AcquireByAuthCodeOption) (AuthResult, error) {
o := acquireTokenByAuthCodeOptions{}
if err := options.ApplyOptions(&o, opts); err != nil {
return AuthResult{}, err
}
params := base.AcquireTokenAuthCodeParameters{
Scopes: scopes,
Code: code,
Challenge: o.challenge,
Claims: o.claims,
AppType: accesstokens.ATPublic,
RedirectURI: redirectURI,
TenantID: o.tenantID,
}
return pca.base.AcquireTokenByAuthCode(ctx, params)
}
// Accounts gets all the accounts in the token cache.
// If there are no accounts in the cache the returned slice is empty.
func (pca Client) Accounts(ctx context.Context) ([]Account, error) {
return pca.base.AllAccounts(ctx)
}
// RemoveAccount signs the account out and forgets account from token cache.
func (pca Client) RemoveAccount(ctx context.Context, account Account) error {
return pca.base.RemoveAccount(ctx, account)
}
// interactiveAuthOptions contains the optional parameters used to acquire an access token for interactive auth code flow.
type interactiveAuthOptions struct {
claims, domainHint, loginHint, redirectURI, tenantID string
}
// AcquireInteractiveOption is implemented by options for AcquireTokenInteractive
type AcquireInteractiveOption interface {
acquireInteractiveOption()
}
// WithLoginHint pre-populates the login prompt with a username.
func WithLoginHint(username string) interface {
AcquireInteractiveOption
AuthCodeURLOption
options.CallOption
} {
return struct {
AcquireInteractiveOption
AuthCodeURLOption
options.CallOption
}{
CallOption: options.NewCallOption(
func(a any) error {
switch t := a.(type) {
case *authCodeURLOptions:
t.loginHint = username
case *interactiveAuthOptions:
t.loginHint = username
default:
return fmt.Errorf("unexpected options type %T", a)
}
return nil
},
),
}
}
// WithDomainHint adds the IdP domain as domain_hint query parameter in the auth url.
func WithDomainHint(domain string) interface {
AcquireInteractiveOption
AuthCodeURLOption
options.CallOption
} {
return struct {
AcquireInteractiveOption
AuthCodeURLOption
options.CallOption
}{
CallOption: options.NewCallOption(
func(a any) error {
switch t := a.(type) {
case *authCodeURLOptions:
t.domainHint = domain
case *interactiveAuthOptions:
t.domainHint = domain
default:
return fmt.Errorf("unexpected options type %T", a)
}
return nil
},
),
}
}
// WithRedirectURI sets a port for the local server used in interactive authentication, for
// example http://localhost:port. All URI components other than the port are ignored.
func WithRedirectURI(redirectURI string) interface {
AcquireInteractiveOption
options.CallOption
} {
return struct {
AcquireInteractiveOption
options.CallOption
}{
CallOption: options.NewCallOption(
func(a any) error {
switch t := a.(type) {
case *interactiveAuthOptions:
t.redirectURI = redirectURI
default:
return fmt.Errorf("unexpected options type %T", a)
}
return nil
},
),
}
}
// AcquireTokenInteractive acquires a security token from the authority using the default web browser to select the account.
// https://docs.microsoft.com/en-us/azure/active-directory/develop/msal-authentication-flows#interactive-and-non-interactive-authentication
//
// Options: [WithDomainHint], [WithLoginHint], [WithRedirectURI], [WithTenantID]
func (pca Client) AcquireTokenInteractive(ctx context.Context, scopes []string, opts ...AcquireInteractiveOption) (AuthResult, error) {
o := interactiveAuthOptions{}
if err := options.ApplyOptions(&o, opts); err != nil {
return AuthResult{}, err
}
// the code verifier is a random 32-byte sequence that's been base-64 encoded without padding.
// it's used to prevent MitM attacks during auth code flow, see https://tools.ietf.org/html/rfc7636
cv, challenge, err := codeVerifier()
if err != nil {
return AuthResult{}, err
}
var redirectURL *url.URL
if o.redirectURI != "" {
redirectURL, err = url.Parse(o.redirectURI)
if err != nil {
return AuthResult{}, err
}
}
authParams, err := pca.base.AuthParams.WithTenant(o.tenantID)
if err != nil {
return AuthResult{}, err
}
authParams.Scopes = scopes
authParams.AuthorizationType = authority.ATInteractive
authParams.Claims = o.claims
authParams.CodeChallenge = challenge
authParams.CodeChallengeMethod = "S256"
authParams.LoginHint = o.loginHint
authParams.DomainHint = o.domainHint
authParams.State = uuid.New().String()
authParams.Prompt = "select_account"
res, err := pca.browserLogin(ctx, redirectURL, authParams)
if err != nil {
return AuthResult{}, err
}
authParams.Redirecturi = res.redirectURI
req, err := accesstokens.NewCodeChallengeRequest(authParams, accesstokens.ATPublic, nil, res.authCode, cv)
if err != nil {
return AuthResult{}, err
}
token, err := pca.base.Token.AuthCode(ctx, req)
if err != nil {
return AuthResult{}, err
}
return pca.base.AuthResultFromToken(ctx, authParams, token, true)
}
type interactiveAuthResult struct {
authCode string
redirectURI string
}
// provides a test hook to simulate opening a browser
var browserOpenURL = func(authURL string) error {
return browser.OpenURL(authURL)
}
// parses the port number from the provided URL.
// returns 0 if nil or no port is specified.
func parsePort(u *url.URL) (int, error) {
if u == nil {
return 0, nil
}
p := u.Port()
if p == "" {
return 0, nil
}
return strconv.Atoi(p)
}
// browserLogin launches the system browser for interactive login
func (pca Client) browserLogin(ctx context.Context, redirectURI *url.URL, params authority.AuthParams) (interactiveAuthResult, error) {
// start local redirect server so login can call us back
port, err := parsePort(redirectURI)
if err != nil {
return interactiveAuthResult{}, err
}
srv, err := local.New(params.State, port)
if err != nil {
return interactiveAuthResult{}, err
}
defer srv.Shutdown()
params.Scopes = accesstokens.AppendDefaultScopes(params)
authURL, err := pca.base.AuthCodeURL(ctx, params.ClientID, srv.Addr, params.Scopes, params)
if err != nil {
return interactiveAuthResult{}, err
}
// open browser window so user can select credentials
if err := browserOpenURL(authURL); err != nil {
return interactiveAuthResult{}, err
}
// now wait until the logic calls us back
res := srv.Result(ctx)
if res.Err != nil {
return interactiveAuthResult{}, res.Err
}
return interactiveAuthResult{
authCode: res.Code,
redirectURI: srv.Addr,
}, nil
}
// creates a code verifier string along with its SHA256 hash which
// is used as the challenge when requesting an auth code.
// used in interactive auth flow for PKCE.
func codeVerifier() (codeVerifier string, challenge string, err error) {
cvBytes := make([]byte, 32)
if _, err = rand.Read(cvBytes); err != nil {
return
}
codeVerifier = base64.RawURLEncoding.EncodeToString(cvBytes)
// for PKCE, create a hash of the code verifier
cvh := sha256.Sum256([]byte(codeVerifier))
challenge = base64.RawURLEncoding.EncodeToString(cvh[:])
return
}

View file

@ -1,3 +1,543 @@
# Release (2023-06-13)
## General Highlights
* **Dependency Update**: Updated to the latest SDK module versions
## Module Highlights
* `github.com/aws/aws-sdk-go-v2/service/cloudtrail`: [v1.27.0](service/cloudtrail/CHANGELOG.md#v1270-2023-06-13)
* **Feature**: This feature allows users to view dashboards for CloudTrail Lake event data stores.
* `github.com/aws/aws-sdk-go-v2/service/codegurusecurity`: [v1.0.0](service/codegurusecurity/CHANGELOG.md#v100-2023-06-13)
* **Release**: New AWS service client module
* **Feature**: Initial release of Amazon CodeGuru Security APIs
* `github.com/aws/aws-sdk-go-v2/service/drs`: [v1.14.0](service/drs/CHANGELOG.md#v1140-2023-06-13)
* **Feature**: Added APIs to support network replication and recovery using AWS Elastic Disaster Recovery.
* `github.com/aws/aws-sdk-go-v2/service/ec2`: [v1.100.0](service/ec2/CHANGELOG.md#v11000-2023-06-13)
* **Feature**: This release introduces a new feature, EC2 Instance Connect Endpoint, that enables you to connect to a resource over TCP, without requiring the resource to have a public IPv4 address.
* `github.com/aws/aws-sdk-go-v2/service/imagebuilder`: [v1.23.5](service/imagebuilder/CHANGELOG.md#v1235-2023-06-13)
* **Documentation**: Change the Image Builder ImagePipeline dateNextRun field to more accurately describe the data.
* `github.com/aws/aws-sdk-go-v2/service/lightsail`: [v1.27.0](service/lightsail/CHANGELOG.md#v1270-2023-06-13)
* **Feature**: This release adds pagination for the Get Certificates API operation.
* `github.com/aws/aws-sdk-go-v2/service/s3`: [v1.34.0](service/s3/CHANGELOG.md#v1340-2023-06-13)
* **Feature**: Integrate double encryption feature to SDKs.
* **Bug Fix**: Fix HeadObject to return types.Nound when an object does not exist. Fixes [2084](https://github.com/aws/aws-sdk-go-v2/issues/2084)
* `github.com/aws/aws-sdk-go-v2/service/securityhub`: [v1.33.0](service/securityhub/CHANGELOG.md#v1330-2023-06-13)
* **Feature**: Add support for Security Hub Automation Rules
* `github.com/aws/aws-sdk-go-v2/service/simspaceweaver`: [v1.3.0](service/simspaceweaver/CHANGELOG.md#v130-2023-06-13)
* **Feature**: This release fixes using aws-us-gov ARNs in API calls and adds documentation for snapshot APIs.
* `github.com/aws/aws-sdk-go-v2/service/verifiedpermissions`: [v1.0.0](service/verifiedpermissions/CHANGELOG.md#v100-2023-06-13)
* **Release**: New AWS service client module
* **Feature**: GA release of Amazon Verified Permissions.
* `github.com/aws/aws-sdk-go-v2/service/wafv2`: [v1.35.0](service/wafv2/CHANGELOG.md#v1350-2023-06-13)
* **Feature**: You can now detect and block fraudulent account creation attempts with the new AWS WAF Fraud Control account creation fraud prevention (ACFP) managed rule group AWSManagedRulesACFPRuleSet.
* `github.com/aws/aws-sdk-go-v2/service/wellarchitected`: [v1.21.0](service/wellarchitected/CHANGELOG.md#v1210-2023-06-13)
* **Feature**: AWS Well-Architected now supports Profiles that help customers prioritize which questions to focus on first by providing a list of prioritized questions that are better aligned with their business goals and outcomes.
# Release (2023-06-12)
## General Highlights
* **Dependency Update**: Updated to the latest SDK module versions
## Module Highlights
* `github.com/aws/aws-sdk-go-v2/service/amplifyuibuilder`: [v1.11.0](service/amplifyuibuilder/CHANGELOG.md#v1110-2023-06-12)
* **Feature**: AWS Amplify UIBuilder is launching Codegen UI, a new feature that enables you to generate your amplify uibuilder components and forms.
* `github.com/aws/aws-sdk-go-v2/service/dynamodb`: [v1.19.8](service/dynamodb/CHANGELOG.md#v1198-2023-06-12)
* **Documentation**: Documentation updates for DynamoDB
* `github.com/aws/aws-sdk-go-v2/service/dynamodbstreams`: [v1.14.12](service/dynamodbstreams/CHANGELOG.md#v11412-2023-06-12)
* **Documentation**: Documentation updates for DynamoDB Streams
* `github.com/aws/aws-sdk-go-v2/service/fsx`: [v1.29.0](service/fsx/CHANGELOG.md#v1290-2023-06-12)
* **Feature**: Amazon FSx for NetApp ONTAP now supports joining a storage virtual machine (SVM) to Active Directory after the SVM has been created.
* `github.com/aws/aws-sdk-go-v2/service/opensearch`: [v1.18.0](service/opensearch/CHANGELOG.md#v1180-2023-06-12)
* **Feature**: This release adds support for SkipUnavailable connection property for cross cluster search
* `github.com/aws/aws-sdk-go-v2/service/rekognition`: [v1.29.0](service/rekognition/CHANGELOG.md#v1290-2023-06-12)
* **Feature**: This release adds support for improved accuracy with user vector in Amazon Rekognition Face Search. Adds new APIs: AssociateFaces, CreateUser, DeleteUser, DisassociateFaces, ListUsers, SearchUsers, SearchUsersByImage. Also adds new face metadata that can be stored: user vector.
* `github.com/aws/aws-sdk-go-v2/service/sagemaker`: [v1.84.0](service/sagemaker/CHANGELOG.md#v1840-2023-06-12)
* **Feature**: Sagemaker Neo now supports compilation for inferentia2 (ML_INF2) and Trainium1 (ML_TRN1) as available targets. With these devices, you can run your workloads at highest performance with lowest cost. inferentia2 (ML_INF2) is available in CMH and Trainium1 (ML_TRN1) is available in IAD currently
# Release (2023-06-09)
## Module Highlights
* `github.com/aws/aws-sdk-go-v2/service/acmpca`: [v1.21.13](service/acmpca/CHANGELOG.md#v12113-2023-06-09)
* **Documentation**: Document-only update to refresh CLI documentation for AWS Private CA. No change to the service.
* `github.com/aws/aws-sdk-go-v2/service/connect`: [v1.58.0](service/connect/CHANGELOG.md#v1580-2023-06-09)
* **Feature**: This release adds search APIs for Prompts, Quick Connects and Hours of Operations, which can be used to search for those resources within a Connect Instance.
# Release (2023-06-08)
## Module Highlights
* `github.com/aws/aws-sdk-go-v2/service/athena`: [v1.30.0](service/athena/CHANGELOG.md#v1300-2023-06-08)
* **Feature**: You can now define custom spark properties at start of the session for use cases like cluster encryption, table formats, and general Spark tuning.
* `github.com/aws/aws-sdk-go-v2/service/comprehendmedical`: [v1.16.0](service/comprehendmedical/CHANGELOG.md#v1160-2023-06-08)
* **Feature**: This release supports a new set of entities and traits.
* `github.com/aws/aws-sdk-go-v2/service/paymentcryptography`: [v1.0.0](service/paymentcryptography/CHANGELOG.md#v100-2023-06-08)
* **Release**: New AWS service client module
* **Feature**: Initial release of AWS Payment Cryptography Control Plane service for creating and managing cryptographic keys used during card payment processing.
* `github.com/aws/aws-sdk-go-v2/service/paymentcryptographydata`: [v1.0.0](service/paymentcryptographydata/CHANGELOG.md#v100-2023-06-08)
* **Release**: New AWS service client module
* **Feature**: Initial release of AWS Payment Cryptography DataPlane Plane service for performing cryptographic operations typically used during card payment processing.
* `github.com/aws/aws-sdk-go-v2/service/servicecatalog`: [v1.19.0](service/servicecatalog/CHANGELOG.md#v1190-2023-06-08)
* **Feature**: New parameter added in ServiceCatalog DescribeProvisioningArtifact api - IncludeProvisioningArtifactParameters. This parameter can be used to return information about the parameters used to provision the product
* `github.com/aws/aws-sdk-go-v2/service/timestreamwrite`: [v1.17.0](service/timestreamwrite/CHANGELOG.md#v1170-2023-06-08)
* **Feature**: This release adds the capability for customers to define how their data should be partitioned, optimizing for certain access patterns. This definition will take place as a part of the table creation.
# Release (2023-06-07)
## Module Highlights
* `github.com/aws/aws-sdk-go-v2/service/cloudformation`: [v1.29.0](service/cloudformation/CHANGELOG.md#v1290-2023-06-07)
* **Feature**: AWS CloudFormation StackSets is updating the deployment experience for all stackset operations to skip suspended AWS accounts during deployments. StackSets will skip target AWS accounts that are suspended and set the Detailed Status of the corresponding stack instances as SKIPPED_SUSPENDED_ACCOUNT
* `github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs`: [v1.21.0](service/cloudwatchlogs/CHANGELOG.md#v1210-2023-06-07)
* **Feature**: This change adds support for account level data protection policies using 3 new APIs, PutAccountPolicy, DeleteAccountPolicy and DescribeAccountPolicy. DescribeLogGroup API has been modified to indicate if account level policy is applied to the LogGroup via "inheritedProperties" list in the response.
* `github.com/aws/aws-sdk-go-v2/service/customerprofiles`: [v1.25.0](service/customerprofiles/CHANGELOG.md#v1250-2023-06-07)
* **Feature**: This release introduces event stream related APIs.
* `github.com/aws/aws-sdk-go-v2/service/directconnect`: [v1.18.15](service/directconnect/CHANGELOG.md#v11815-2023-06-07)
* **Documentation**: This update corrects the jumbo frames mtu values from 9100 to 8500 for transit virtual interfaces.
* `github.com/aws/aws-sdk-go-v2/service/emrcontainers`: [v1.19.0](service/emrcontainers/CHANGELOG.md#v1190-2023-06-07)
* **Feature**: EMR on EKS adds support for log rotation of Spark container logs with EMR-6.11.0 onwards, to the StartJobRun API.
* `github.com/aws/aws-sdk-go-v2/service/iotdeviceadvisor`: [v1.19.0](service/iotdeviceadvisor/CHANGELOG.md#v1190-2023-06-07)
* **Feature**: AWS IoT Core Device Advisor now supports new Qualification Suite test case list. With this update, customers can more easily create new qualification test suite with an empty rootGroup input.
# Release (2023-06-06)
## Module Highlights
* `github.com/aws/aws-sdk-go-v2/service/connect`: [v1.57.1](service/connect/CHANGELOG.md#v1571-2023-06-06)
* **Documentation**: GetMetricDataV2 API is now available in AWS GovCloud(US) region.
* `github.com/aws/aws-sdk-go-v2/service/emr`: [v1.26.0](service/emr/CHANGELOG.md#v1260-2023-06-06)
* **Feature**: This release provides customers the ability to specify an allocation strategies amongst PRICE_CAPACITY_OPTIMIZED, CAPACITY_OPTIMIZED, LOWEST_PRICE, DIVERSIFIED for Spot instances in Instance Feet cluster. This enables customers to choose an allocation strategy best suited for their workload.
* `github.com/aws/aws-sdk-go-v2/service/iam`: [v1.20.0](service/iam/CHANGELOG.md#v1200-2023-06-06)
* **Feature**: This release updates the AccountAlias regex pattern with the same length restrictions enforced by the length constraint.
* `github.com/aws/aws-sdk-go-v2/service/inspector2`: [v1.14.0](service/inspector2/CHANGELOG.md#v1140-2023-06-06)
* **Feature**: Adds new response properties and request parameters for 'last scanned at' on the ListCoverage operation. This feature allows you to search and view the date of which your resources were last scanned by Inspector.
* `github.com/aws/aws-sdk-go-v2/service/iot`: [v1.38.0](service/iot/CHANGELOG.md#v1380-2023-06-06)
* **Feature**: Adding IoT Device Management Software Package Catalog APIs to register, store, and report system software packages, along with their versions and metadata in a centralized location.
* `github.com/aws/aws-sdk-go-v2/service/lexmodelsv2`: [v1.29.0](service/lexmodelsv2/CHANGELOG.md#v1290-2023-06-06)
* **Feature**: This release adds support for Lex Developers to create test sets and to execute those test-sets against their bots.
* `github.com/aws/aws-sdk-go-v2/service/quicksight`: [v1.37.0](service/quicksight/CHANGELOG.md#v1370-2023-06-06)
* **Feature**: QuickSight support for pivot table field collapse state, radar chart range scale and multiple scope options in conditional formatting.
* `github.com/aws/aws-sdk-go-v2/service/signer`: [v1.15.0](service/signer/CHANGELOG.md#v1150-2023-06-06)
* **Feature**: AWS Signer is launching Container Image Signing, a new feature that enables you to sign and verify container images. This feature enables you to validate that only container images you approve are used in your enterprise.
* `github.com/aws/aws-sdk-go-v2/service/sqs`: [v1.23.0](service/sqs/CHANGELOG.md#v1230-2023-06-06)
* **Feature**: Amazon SQS adds three new APIs - StartMessageMoveTask, CancelMessageMoveTask, and ListMessageMoveTasks to automate redriving messages from dead-letter queues to source queues or a custom destination.
# Release (2023-06-05)
## Module Highlights
* `github.com/aws/aws-sdk-go-v2/service/cloudformation`: [v1.28.0](service/cloudformation/CHANGELOG.md#v1280-2023-06-05)
* **Feature**: AWS CloudFormation StackSets provides customers with three new APIs to activate, deactivate, and describe AWS Organizations trusted access which is needed to get started with service-managed StackSets.
* `github.com/aws/aws-sdk-go-v2/service/ec2`: [v1.99.0](service/ec2/CHANGELOG.md#v1990-2023-06-05)
* **Feature**: Making InstanceTagAttribute as the required parameter for the DeregisterInstanceEventNotificationAttributes and RegisterInstanceEventNotificationAttributes APIs.
* `github.com/aws/aws-sdk-go-v2/service/finspace`: [v1.10.0](service/finspace/CHANGELOG.md#v1100-2023-06-05)
* **Feature**: Releasing new Managed kdb Insights APIs
* `github.com/aws/aws-sdk-go-v2/service/frauddetector`: [v1.25.0](service/frauddetector/CHANGELOG.md#v1250-2023-06-05)
* **Feature**: Added new variable types, new DateTime data type, and new rules engine functions for interacting and working with DateTime data types.
* `github.com/aws/aws-sdk-go-v2/service/keyspaces`: [v1.3.0](service/keyspaces/CHANGELOG.md#v130-2023-06-05)
* **Feature**: This release adds support for MRR GA launch, and includes multiregion support in create-keyspace, get-keyspace, and list-keyspace.
* `github.com/aws/aws-sdk-go-v2/service/kms`: [v1.22.0](service/kms/CHANGELOG.md#v1220-2023-06-05)
* **Feature**: This release includes feature to import customer's asymmetric (RSA and ECC) and HMAC keys into KMS. It also includes feature to allow customers to specify number of days to schedule a KMS key deletion as a policy condition key.
* `github.com/aws/aws-sdk-go-v2/service/lambda`: [v1.35.0](service/lambda/CHANGELOG.md#v1350-2023-06-05)
* **Feature**: Add Ruby 3.2 (ruby3.2) Runtime support to AWS Lambda.
* `github.com/aws/aws-sdk-go-v2/service/mwaa`: [v1.16.0](service/mwaa/CHANGELOG.md#v1160-2023-06-05)
* **Feature**: This release adds ROLLING_BACK and CREATING_SNAPSHOT environment statuses for Amazon MWAA environments.
# Release (2023-06-02)
## Module Highlights
* `github.com/aws/aws-sdk-go-v2/service/athena`: [v1.29.0](service/athena/CHANGELOG.md#v1290-2023-06-02)
* **Feature**: This release introduces the DeleteCapacityReservation API and the ability to manage capacity reservations using CloudFormation
* `github.com/aws/aws-sdk-go-v2/service/cloudtrail`: [v1.26.0](service/cloudtrail/CHANGELOG.md#v1260-2023-06-02)
* **Feature**: This feature allows users to start and stop event ingestion on a CloudTrail Lake event data store.
* `github.com/aws/aws-sdk-go-v2/service/sagemaker`: [v1.83.0](service/sagemaker/CHANGELOG.md#v1830-2023-06-02)
* **Feature**: This release adds Selective Execution feature that allows SageMaker Pipelines users to run selected steps in a pipeline.
* `github.com/aws/aws-sdk-go-v2/service/wafv2`: [v1.34.0](service/wafv2/CHANGELOG.md#v1340-2023-06-02)
* **Feature**: Added APIs to describe managed products. The APIs retrieve information about rule groups that are managed by AWS and by AWS Marketplace sellers.
# Release (2023-06-01)
## Module Highlights
* `github.com/aws/aws-sdk-go-v2/service/alexaforbusiness`: [v1.15.11](service/alexaforbusiness/CHANGELOG.md#v11511-2023-06-01)
* **Documentation**: Alexa for Business has been deprecated and is no longer supported.
* `github.com/aws/aws-sdk-go-v2/service/appflow`: [v1.30.0](service/appflow/CHANGELOG.md#v1300-2023-06-01)
* **Feature**: Added ability to select DataTransferApiType for DescribeConnector and CreateFlow requests when using Async supported connectors. Added supportedDataTransferType to DescribeConnector/DescribeConnectors/ListConnector response.
* `github.com/aws/aws-sdk-go-v2/service/customerprofiles`: [v1.24.0](service/customerprofiles/CHANGELOG.md#v1240-2023-06-01)
* **Feature**: This release introduces calculated attribute related APIs.
* `github.com/aws/aws-sdk-go-v2/service/ivs`: [v1.22.0](service/ivs/CHANGELOG.md#v1220-2023-06-01)
* **Feature**: API Update for IVS Advanced Channel type
* `github.com/aws/aws-sdk-go-v2/service/sagemaker`: [v1.82.1](service/sagemaker/CHANGELOG.md#v1821-2023-06-01)
* **Documentation**: Amazon Sagemaker Autopilot adds support for Parquet file input to NLP text classification jobs.
* `github.com/aws/aws-sdk-go-v2/service/wafv2`: [v1.33.1](service/wafv2/CHANGELOG.md#v1331-2023-06-01)
* **Documentation**: Corrected the information for the header order FieldToMatch setting
# Release (2023-05-31)
## Module Highlights
* `github.com/aws/aws-sdk-go-v2/service/configservice`: [v1.33.0](service/configservice/CHANGELOG.md#v1330-2023-05-31)
* **Feature**: Resource Types Exclusion feature launch by AWS Config
* `github.com/aws/aws-sdk-go-v2/service/frauddetector`: [v1.24.0](service/frauddetector/CHANGELOG.md#v1240-2023-05-31)
* **Feature**: This release enables publishing event predictions from Amazon Fraud Detector (AFD) to Amazon EventBridge. For example, after getting predictions from AFD, Amazon EventBridge rules can be configured to trigger notification through an SNS topic, send a message with SES, or trigger Lambda workflows.
* `github.com/aws/aws-sdk-go-v2/service/healthlake`: [v1.16.0](service/healthlake/CHANGELOG.md#v1160-2023-05-31)
* **Feature**: This release adds a new request parameter to the CreateFHIRDatastore API operation. IdentityProviderConfiguration specifies how you want to authenticate incoming requests to your Healthlake Data Store.
* `github.com/aws/aws-sdk-go-v2/service/m2`: [v1.5.0](service/m2/CHANGELOG.md#v150-2023-05-31)
* **Feature**: Adds an optional create-only 'roleArn' property to Application resources. Enables PS and PO data set org types.
* `github.com/aws/aws-sdk-go-v2/service/rds`: [v1.45.0](service/rds/CHANGELOG.md#v1450-2023-05-31)
* **Feature**: This release adds support for changing the engine for Oracle using the ModifyDbInstance API
* `github.com/aws/aws-sdk-go-v2/service/servicecatalog`: [v1.18.5](service/servicecatalog/CHANGELOG.md#v1185-2023-05-31)
* **Documentation**: Documentation updates for ServiceCatalog.
* `github.com/aws/aws-sdk-go-v2/service/workspacesweb`: [v1.10.0](service/workspacesweb/CHANGELOG.md#v1100-2023-05-31)
* **Feature**: WorkSpaces Web now allows you to control which IP addresses your WorkSpaces Web portal may be accessed from.
# Release (2023-05-30)
## Module Highlights
* `github.com/aws/aws-sdk-go-v2/service/chimesdkvoice`: [v1.6.0](service/chimesdkvoice/CHANGELOG.md#v160-2023-05-30)
* **Feature**: Added optional CallLeg field to StartSpeakerSearchTask API request
* `github.com/aws/aws-sdk-go-v2/service/glue`: [v1.50.0](service/glue/CHANGELOG.md#v1500-2023-05-30)
* **Feature**: Added Runtime parameter to allow selection of Ray Runtime
* `github.com/aws/aws-sdk-go-v2/service/groundstation`: [v1.18.3](service/groundstation/CHANGELOG.md#v1183-2023-05-30)
* **Documentation**: Updating description of GetMinuteUsage to be clearer.
* `github.com/aws/aws-sdk-go-v2/service/iotfleetwise`: [v1.4.0](service/iotfleetwise/CHANGELOG.md#v140-2023-05-30)
* **Feature**: Campaigns now support selecting Timestream or S3 as the data destination, Signal catalogs now support "Deprecation" keyword released in VSS v2.1 and "Comment" keyword released in VSS v3.0
* `github.com/aws/aws-sdk-go-v2/service/location`: [v1.23.0](service/location/CHANGELOG.md#v1230-2023-05-30)
* **Feature**: This release adds API support for political views for the maps service APIs: CreateMap, UpdateMap, DescribeMap.
* `github.com/aws/aws-sdk-go-v2/service/memorydb`: [v1.13.0](service/memorydb/CHANGELOG.md#v1130-2023-05-30)
* **Feature**: Amazon MemoryDB for Redis now supports AWS Identity and Access Management authentication access to Redis clusters starting with redis-engine version 7.0
* `github.com/aws/aws-sdk-go-v2/service/personalize`: [v1.24.0](service/personalize/CHANGELOG.md#v1240-2023-05-30)
* **Feature**: This release provides support for the exclusion of certain columns for training when creating a solution and creating or updating a recommender with Amazon Personalize.
* `github.com/aws/aws-sdk-go-v2/service/polly`: [v1.26.0](service/polly/CHANGELOG.md#v1260-2023-05-30)
* **Feature**: Amazon Polly adds 2 new voices - Sofie (da-DK) and Niamh (en-IE)
* `github.com/aws/aws-sdk-go-v2/service/securityhub`: [v1.32.0](service/securityhub/CHANGELOG.md#v1320-2023-05-30)
* **Feature**: Added new resource detail objects to ASFF, including resources for AwsGuardDutyDetector, AwsAmazonMqBroker, AwsEventSchemasRegistry, AwsAppSyncGraphQlApi and AwsStepFunctionStateMachine.
* `github.com/aws/aws-sdk-go-v2/service/securitylake`: [v1.4.0](service/securitylake/CHANGELOG.md#v140-2023-05-30)
* **Feature**: Log sources are now versioned. AWS log sources and custom sources will now come with a version identifier that enables producers to vend multiple schema versions to subscribers. Security Lake API have been refactored to more closely align with AWS API conventions.
* `github.com/aws/aws-sdk-go-v2/service/wafv2`: [v1.33.0](service/wafv2/CHANGELOG.md#v1330-2023-05-30)
* **Feature**: This SDK release provides customers the ability to use Header Order as a field to match.
# Release (2023-05-26)
## Module Highlights
* `github.com/aws/aws-sdk-go-v2/service/connect`: [v1.57.0](service/connect/CHANGELOG.md#v1570-2023-05-26)
* **Feature**: Documentation update for a new Initiation Method value in DescribeContact API
* `github.com/aws/aws-sdk-go-v2/service/iotwireless`: [v1.28.0](service/iotwireless/CHANGELOG.md#v1280-2023-05-26)
* **Feature**: Add Multicast Group support in Network Analyzer Configuration.
* `github.com/aws/aws-sdk-go-v2/service/sagemaker`: [v1.82.0](service/sagemaker/CHANGELOG.md#v1820-2023-05-26)
* **Feature**: Added ml.p4d and ml.inf1 as supported instance type families for SageMaker Notebook Instances.
# Release (2023-05-25)
## Module Highlights
* `github.com/aws/aws-sdk-go-v2/service/applicationautoscaling`: [v1.21.0](service/applicationautoscaling/CHANGELOG.md#v1210-2023-05-25)
* **Feature**: With this release, ElastiCache customers will be able to use predefined metricType "ElastiCacheDatabaseCapacityUsageCountedForEvictPercentage" for their ElastiCache instances.
* `github.com/aws/aws-sdk-go-v2/service/codepipeline`: [v1.15.0](service/codepipeline/CHANGELOG.md#v1150-2023-05-25)
* **Feature**: Add PollingDisabledAt time information in PipelineMetadata object of GetPipeline API.
* `github.com/aws/aws-sdk-go-v2/service/gamelift`: [v1.19.0](service/gamelift/CHANGELOG.md#v1190-2023-05-25)
* **Feature**: GameLift FleetIQ users can now filter game server claim requests to exclude servers on instances that are draining.
* `github.com/aws/aws-sdk-go-v2/service/glue`: [v1.49.0](service/glue/CHANGELOG.md#v1490-2023-05-25)
* **Feature**: Added ability to create data quality rulesets for shared, cross-account Glue Data Catalog tables. Added support for dataset comparison rules through a new parameter called AdditionalDataSources. Enhanced the data quality results with a map containing profiled metric values.
* `github.com/aws/aws-sdk-go-v2/service/migrationhubrefactorspaces`: [v1.10.0](service/migrationhubrefactorspaces/CHANGELOG.md#v1100-2023-05-25)
* **Feature**: This SDK update allows for path parameter syntax to be passed to the CreateRoute API. Path parameter syntax require parameters to be enclosed in {} characters. This update also includes a new AppendSourcePath field which lets users forward the source path to the Service URL endpoint.
* `github.com/aws/aws-sdk-go-v2/service/sagemaker`: [v1.81.0](service/sagemaker/CHANGELOG.md#v1810-2023-05-25)
* **Feature**: Amazon SageMaker Automatic Model Tuning now supports enabling Autotune for tuning jobs which can choose tuning job configurations.
# Release (2023-05-24)
## Module Highlights
* `github.com/aws/aws-sdk-go-v2/service/appsync`: [v1.21.0](service/appsync/CHANGELOG.md#v1210-2023-05-24)
* **Feature**: This release introduces AppSync Merged APIs, which provide the ability to compose multiple source APIs into a single federated/merged API.
* `github.com/aws/aws-sdk-go-v2/service/connect`: [v1.56.0](service/connect/CHANGELOG.md#v1560-2023-05-24)
* **Feature**: Amazon Connect Evaluation Capabilities: validation improvements
* `github.com/aws/aws-sdk-go-v2/service/costandusagereportservice`: [v1.16.0](service/costandusagereportservice/CHANGELOG.md#v1160-2023-05-24)
* **Feature**: Add support for split cost allocation data on a report.
* `github.com/aws/aws-sdk-go-v2/service/sagemaker`: [v1.80.0](service/sagemaker/CHANGELOG.md#v1800-2023-05-24)
* **Feature**: SageMaker now provides an instantaneous deployment recommendation through the DescribeModel API
# Release (2023-05-23)
## Module Highlights
* `github.com/aws/aws-sdk-go-v2/service/fms`: [v1.24.0](service/fms/CHANGELOG.md#v1240-2023-05-23)
* **Feature**: Fixes issue that could cause calls to GetAdminScope and ListAdminAccountsForOrganization to return a 500 Internal Server error.
* `github.com/aws/aws-sdk-go-v2/service/sagemaker`: [v1.79.0](service/sagemaker/CHANGELOG.md#v1790-2023-05-23)
* **Feature**: Added ModelNameEquals, ModelPackageVersionArnEquals in request and ModelName, SamplePayloadUrl, ModelPackageVersionArn in response of ListInferenceRecommendationsJobs API. Added Invocation timestamps in response of DescribeInferenceRecommendationsJob API & ListInferenceRecommendationsJobSteps API.
* `github.com/aws/aws-sdk-go-v2/service/translate`: [v1.18.0](service/translate/CHANGELOG.md#v1180-2023-05-23)
* **Feature**: Added support for calling TranslateDocument API.
# Release (2023-05-22)
## Module Highlights
* `github.com/aws/aws-sdk-go-v2/service/backup`: [v1.22.0](service/backup/CHANGELOG.md#v1220-2023-05-22)
* **Feature**: Added support for tags on restore.
* `github.com/aws/aws-sdk-go-v2/service/pinpoint`: [v1.19.2](service/pinpoint/CHANGELOG.md#v1192-2023-05-22)
* **Documentation**: Amazon Pinpoint is deprecating the tags parameter in the UpdateSegment, UpdateCampaign, UpdateEmailTemplate, UpdateSmsTemplate, UpdatePushTemplate, UpdateInAppTemplate and UpdateVoiceTemplate. Amazon Pinpoint will end support tags parameter by May 22, 2023.
* `github.com/aws/aws-sdk-go-v2/service/quicksight`: [v1.36.0](service/quicksight/CHANGELOG.md#v1360-2023-05-22)
* **Feature**: Add support for Asset Bundle, Geospatial Heatmaps.
# Release (2023-05-19)
## Module Highlights
* `github.com/aws/aws-sdk-go-v2/service/backup`: [v1.21.0](service/backup/CHANGELOG.md#v1210-2023-05-19)
* **Feature**: Add ResourceArn, ResourceType, and BackupVaultName to ListRecoveryPointsByLegalHold API response.
* `github.com/aws/aws-sdk-go-v2/service/connectcases`: [v1.4.0](service/connectcases/CHANGELOG.md#v140-2023-05-19)
* **Feature**: This release adds the ability to create fields with type Url through the CreateField API. For more information see https://docs.aws.amazon.com/cases/latest/APIReference/Welcome.html
* `github.com/aws/aws-sdk-go-v2/service/mediapackagev2`: [v1.0.0](service/mediapackagev2/CHANGELOG.md#v100-2023-05-19)
* **Release**: New AWS service client module
* **Feature**: Adds support for the MediaPackage Live v2 API
* `github.com/aws/aws-sdk-go-v2/service/sesv2`: [v1.18.0](service/sesv2/CHANGELOG.md#v1180-2023-05-19)
* **Feature**: This release allows customers to update scaling mode property of dedicated IP pools with PutDedicatedIpPoolScalingAttributes call.
# Release (2023-05-18)
## Module Highlights
* `github.com/aws/aws-sdk-go-v2/service/athena`: [v1.28.0](service/athena/CHANGELOG.md#v1280-2023-05-18)
* **Feature**: Removing SparkProperties from EngineConfiguration object for StartSession API call
* `github.com/aws/aws-sdk-go-v2/service/cloudtrail`: [v1.25.0](service/cloudtrail/CHANGELOG.md#v1250-2023-05-18)
* **Feature**: Add ConflictException to PutEventSelectors, add (Channel/EDS)ARNInvalidException to Tag APIs. These exceptions provide customers with more specific error messages instead of internal errors.
* `github.com/aws/aws-sdk-go-v2/service/computeoptimizer`: [v1.24.0](service/computeoptimizer/CHANGELOG.md#v1240-2023-05-18)
* **Feature**: In this launch, we add support for showing integration status with external metric providers such as Instana, Datadog ...etc in GetEC2InstanceRecommendations and ExportEC2InstanceRecommendations apis
* `github.com/aws/aws-sdk-go-v2/service/connect`: [v1.55.0](service/connect/CHANGELOG.md#v1550-2023-05-18)
* **Feature**: You can programmatically create and manage prompts using APIs, for example, to extract prompts stored within Amazon Connect and add them to your Amazon S3 bucket. AWS CloudTrail, AWS CloudFormation and tagging are supported.
* `github.com/aws/aws-sdk-go-v2/service/ec2`: [v1.98.0](service/ec2/CHANGELOG.md#v1980-2023-05-18)
* **Feature**: Add support for i4g.large, i4g.xlarge, i4g.2xlarge, i4g.4xlarge, i4g.8xlarge and i4g.16xlarge instances powered by AWS Graviton2 processors that deliver up to 15% better compute performance than our other storage-optimized instances.
* `github.com/aws/aws-sdk-go-v2/service/ecs`: [v1.27.1](service/ecs/CHANGELOG.md#v1271-2023-05-18)
* **Documentation**: Documentation only release to address various tickets.
* `github.com/aws/aws-sdk-go-v2/service/mediaconvert`: [v1.37.0](service/mediaconvert/CHANGELOG.md#v1370-2023-05-18)
* **Feature**: This release introduces a new MXF Profile for XDCAM which is strictly compliant with the SMPTE RDD 9 standard and improved handling of output name modifiers.
* `github.com/aws/aws-sdk-go-v2/service/rds`: [v1.44.1](service/rds/CHANGELOG.md#v1441-2023-05-18)
* **Documentation**: RDS documentation update for the EngineVersion parameter of ModifyDBSnapshot
* `github.com/aws/aws-sdk-go-v2/service/sagemakergeospatial`: [v1.3.0](service/sagemakergeospatial/CHANGELOG.md#v130-2023-05-18)
* **Feature**: This release makes ExecutionRoleArn a required field in the StartEarthObservationJob API.
# Release (2023-05-16)
## Module Highlights
* `github.com/aws/aws-sdk-go-v2/service/detective`: [v1.19.0](service/detective/CHANGELOG.md#v1190-2023-05-16)
* **Feature**: Added and updated API operations in Detective to support the integration of ASFF Security Hub findings.
* `github.com/aws/aws-sdk-go-v2/service/directconnect`: [v1.18.14](service/directconnect/CHANGELOG.md#v11814-2023-05-16)
* **Documentation**: This release includes an update to the mtu value for CreateTransitVirtualInterface from 9001 mtu to 8500 mtu.
* `github.com/aws/aws-sdk-go-v2/service/glue`: [v1.48.0](service/glue/CHANGELOG.md#v1480-2023-05-16)
* **Feature**: Add Support for Tags for Custom Entity Types
* `github.com/aws/aws-sdk-go-v2/service/secretsmanager`: [v1.19.8](service/secretsmanager/CHANGELOG.md#v1198-2023-05-16)
* **Documentation**: Documentation updates for Secrets Manager
* `github.com/aws/aws-sdk-go-v2/service/wafv2`: [v1.32.0](service/wafv2/CHANGELOG.md#v1320-2023-05-16)
* **Feature**: My AWS Service (placeholder) - You can now rate limit web requests based on aggregation keys other than IP addresses, and you can aggregate using combinations of keys. You can also rate limit all requests that match a scope-down statement, without further aggregation.
# Release (2023-05-15)
## Module Highlights
* `github.com/aws/aws-sdk-go-v2/service/athena`: [v1.27.0](service/athena/CHANGELOG.md#v1270-2023-05-15)
* **Feature**: You can now define custom spark properties at start of the session for use cases like cluster encryption, table formats, and general Spark tuning.
* `github.com/aws/aws-sdk-go-v2/service/codecatalyst`: [v1.3.0](service/codecatalyst/CHANGELOG.md#v130-2023-05-15)
* **Feature**: With this release, the users can list the active sessions connected to their Dev Environment on AWS CodeCatalyst
* `github.com/aws/aws-sdk-go-v2/service/rekognition`: [v1.28.0](service/rekognition/CHANGELOG.md#v1280-2023-05-15)
* **Feature**: This release adds a new EyeDirection attribute in Amazon Rekognition DetectFaces and IndexFaces APIs which predicts the yaw and pitch angles of a person's eye gaze direction for each face detected in the image.
* `github.com/aws/aws-sdk-go-v2/service/rolesanywhere`: [v1.2.0](service/rolesanywhere/CHANGELOG.md#v120-2023-05-15)
* **Feature**: Adds support for custom notification settings in a trust anchor. Introduces PutNotificationSettings and ResetNotificationSettings API's. Updates DurationSeconds max value to 3600.
* `github.com/aws/aws-sdk-go-v2/service/transfer`: [v1.29.0](service/transfer/CHANGELOG.md#v1290-2023-05-15)
* **Feature**: This release introduces the ability to require both password and SSH key when users authenticate to your Transfer Family servers that use the SFTP protocol.
# Release (2023-05-11)
## Module Highlights
* `github.com/aws/aws-sdk-go-v2/service/connect`: [v1.54.2](service/connect/CHANGELOG.md#v1542-2023-05-11)
* **Documentation**: This release updates GetMetricDataV2 API, to support metric data up-to last 35 days
* `github.com/aws/aws-sdk-go-v2/service/elasticache`: [v1.27.0](service/elasticache/CHANGELOG.md#v1270-2023-05-11)
* **Feature**: Added support to modify the cluster mode configuration for the existing ElastiCache ReplicationGroups. Customers can now modify the configuration from cluster mode disabled to cluster mode enabled.
* `github.com/aws/aws-sdk-go-v2/service/elasticsearchservice`: [v1.19.0](service/elasticsearchservice/CHANGELOG.md#v1190-2023-05-11)
* **Feature**: This release fixes DescribePackages API error with null filter value parameter.
* `github.com/aws/aws-sdk-go-v2/service/health`: [v1.17.0](service/health/CHANGELOG.md#v1170-2023-05-11)
* **Feature**: Add support for regional endpoints
* `github.com/aws/aws-sdk-go-v2/service/ivsrealtime`: [v1.2.0](service/ivsrealtime/CHANGELOG.md#v120-2023-05-11)
* **Feature**: Add methods for inspecting and debugging stages: ListStageSessions, GetStageSession, ListParticipants, GetParticipant, and ListParticipantEvents.
* `github.com/aws/aws-sdk-go-v2/service/omics`: [v1.4.0](service/omics/CHANGELOG.md#v140-2023-05-11)
* **Feature**: This release provides support for Ready2Run and GPU workflows, an improved read set filter, the direct upload of read sets into Omics Storage, and annotation parsing for analytics stores.
* `github.com/aws/aws-sdk-go-v2/service/support`: [v1.15.0](service/support/CHANGELOG.md#v1150-2023-05-11)
* **Feature**: This release adds 2 new Support APIs, DescribeCreateCaseOptions and DescribeSupportedLanguages. You can use these new APIs to get available support languages.
# Release (2023-05-10)
## Module Highlights
* `github.com/aws/aws-sdk-go-v2/service/emr`: [v1.25.0](service/emr/CHANGELOG.md#v1250-2023-05-10)
* **Feature**: EMR Studio now supports programmatically executing a Notebooks on an EMR on EKS cluster. In addition, notebooks can now be executed by specifying its location in S3.
* `github.com/aws/aws-sdk-go-v2/service/rds`: [v1.44.0](service/rds/CHANGELOG.md#v1440-2023-05-10)
* **Feature**: Amazon Relational Database Service (RDS) updates for the new Aurora I/O-Optimized storage type for Amazon Aurora DB clusters
* `github.com/aws/aws-sdk-go-v2/service/swf`: [v1.15.0](service/swf/CHANGELOG.md#v1150-2023-05-10)
* **Feature**: This release adds a new API parameter to exclude old history events from decision tasks.
# Release (2023-05-09)
## General Highlights
* **Dependency Update**: Updated to the latest SDK module versions
## Module Highlights
* `github.com/aws/aws-sdk-go-v2/service/applicationautoscaling`: [v1.20.0](service/applicationautoscaling/CHANGELOG.md#v1200-2023-05-09)
* **Feature**: With this release, Amazon SageMaker Serverless Inference customers can use Application Auto Scaling to auto scale the provisioned concurrency of their serverless endpoints.
* `github.com/aws/aws-sdk-go-v2/service/glue`: [v1.47.0](service/glue/CHANGELOG.md#v1470-2023-05-09)
* **Feature**: This release adds AmazonRedshift Source and Target nodes in addition to DynamicTransform OutputSchemas
* `github.com/aws/aws-sdk-go-v2/service/sagemaker`: [v1.78.0](service/sagemaker/CHANGELOG.md#v1780-2023-05-09)
* **Feature**: This release includes support for (1) Provisioned Concurrency for Amazon SageMaker Serverless Inference and (2) UpdateEndpointWeightsAndCapacities API for Serverless endpoints.
# Release (2023-05-08)
## General Highlights
* **Dependency Update**: Updated to the latest SDK module versions
## Module Highlights
* `github.com/aws/aws-sdk-go-v2/service/glue`: [v1.46.0](service/glue/CHANGELOG.md#v1460-2023-05-08)
* **Feature**: Support large worker types G.4x and G.8x for Glue Spark
* `github.com/aws/aws-sdk-go-v2/service/guardduty`: [v1.23.0](service/guardduty/CHANGELOG.md#v1230-2023-05-08)
* **Feature**: Add AccessDeniedException 403 Error message code to support 3 Tagging related APIs
* `github.com/aws/aws-sdk-go-v2/service/iotsitewise`: [v1.29.0](service/iotsitewise/CHANGELOG.md#v1290-2023-05-08)
* **Feature**: Provide support for 20,000 max results for GetAssetPropertyValueHistory/BatchGetAssetPropertyValueHistory and 15 minute aggregate resolution for GetAssetPropertyAggregates/BatchGetAssetPropertyAggregates
* `github.com/aws/aws-sdk-go-v2/service/sts`: [v1.19.0](service/sts/CHANGELOG.md#v1190-2023-05-08)
* **Feature**: Documentation updates for AWS Security Token Service.
# Release (2023-05-05)
## Module Highlights
* `github.com/aws/aws-sdk-go-v2/service/ec2`: [v1.97.0](service/ec2/CHANGELOG.md#v1970-2023-05-05)
* **Feature**: This release adds support the inf2 and trn1n instances. inf2 instances are purpose built for deep learning inference while trn1n instances are powered by AWS Trainium accelerators and they build on the capabilities of Trainium-powered trn1 instances.
* `github.com/aws/aws-sdk-go-v2/service/inspector2`: [v1.13.0](service/inspector2/CHANGELOG.md#v1130-2023-05-05)
* **Feature**: Amazon Inspector now allows customers to search its vulnerability intelligence database if any of the Inspector scanning types are activated.
* `github.com/aws/aws-sdk-go-v2/service/mediatailor`: [v1.23.0](service/mediatailor/CHANGELOG.md#v1230-2023-05-05)
* **Feature**: This release adds support for AFTER_LIVE_EDGE mode configuration for avail suppression, and adding a fill-policy setting that sets the avail suppression to PARTIAL_AVAIL or FULL_AVAIL_ONLY when AFTER_LIVE_EDGE is enabled.
* `github.com/aws/aws-sdk-go-v2/service/sqs`: [v1.22.0](service/sqs/CHANGELOG.md#v1220-2023-05-05)
* **Feature**: Revert previous SQS protocol change.
# Release (2023-05-04)
## General Highlights
* **Dependency Update**: Updated to the latest SDK module versions
## Module Highlights
* `github.com/aws/aws-sdk-go-v2/service/cloudwatch`: [v1.26.0](service/cloudwatch/CHANGELOG.md#v1260-2023-05-04)
* **Feature**: Adds support for filtering by metric names in CloudWatch Metric Streams.
* `github.com/aws/aws-sdk-go-v2/service/configservice`: [v1.32.0](service/configservice/CHANGELOG.md#v1320-2023-05-04)
* **Feature**: Updated ResourceType enum with new resource types onboarded by AWS Config in April 2023.
* `github.com/aws/aws-sdk-go-v2/service/connect`: [v1.54.1](service/connect/CHANGELOG.md#v1541-2023-05-04)
* **Documentation**: Remove unused InvalidParameterException from CreateParticipant API
* `github.com/aws/aws-sdk-go-v2/service/ecs`: [v1.27.0](service/ecs/CHANGELOG.md#v1270-2023-05-04)
* **Feature**: Documentation update for new error type NamespaceNotFoundException for CreateCluster and UpdateCluster
* `github.com/aws/aws-sdk-go-v2/service/networkfirewall`: [v1.28.0](service/networkfirewall/CHANGELOG.md#v1280-2023-05-04)
* **Feature**: This release adds support for the Suricata REJECT option in midstream exception configurations.
* `github.com/aws/aws-sdk-go-v2/service/opensearch`: [v1.17.0](service/opensearch/CHANGELOG.md#v1170-2023-05-04)
* **Feature**: DescribeDomainNodes: A new API that provides configuration information for nodes part of the domain
* `github.com/aws/aws-sdk-go-v2/service/quicksight`: [v1.35.0](service/quicksight/CHANGELOG.md#v1350-2023-05-04)
* **Feature**: Add support for Topic, Dataset parameters and VPC
* `github.com/aws/aws-sdk-go-v2/service/rekognition`: [v1.27.0](service/rekognition/CHANGELOG.md#v1270-2023-05-04)
* **Feature**: This release adds a new attribute FaceOccluded. Additionally, you can now select attributes individually (e.g. ["DEFAULT", "FACE_OCCLUDED", "AGE_RANGE"] instead of ["ALL"]), which can reduce response time.
* `github.com/aws/aws-sdk-go-v2/service/s3`: [v1.33.1](service/s3/CHANGELOG.md#v1331-2023-05-04)
* **Documentation**: Documentation updates for Amazon S3
* `github.com/aws/aws-sdk-go-v2/service/sagemaker`: [v1.77.0](service/sagemaker/CHANGELOG.md#v1770-2023-05-04)
* **Feature**: We added support for ml.inf2 and ml.trn1 family of instances on Amazon SageMaker for deploying machine learning (ML) models for Real-time and Asynchronous inference. You can use these instances to achieve high performance at a low cost for generative artificial intelligence (AI) models.
* `github.com/aws/aws-sdk-go-v2/service/securityhub`: [v1.31.0](service/securityhub/CHANGELOG.md#v1310-2023-05-04)
* **Feature**: Add support for Finding History.
* `github.com/aws/aws-sdk-go-v2/service/sqs`: [v1.21.0](service/sqs/CHANGELOG.md#v1210-2023-05-04)
* **Feature**: This release enables customers to call SQS using AWS JSON-1.0 protocol.
# Release (2023-05-03)
## Module Highlights
* `github.com/aws/aws-sdk-go-v2/service/appsync`: [v1.20.0](service/appsync/CHANGELOG.md#v1200-2023-05-03)
* **Feature**: Private API support for AWS AppSync. With Private APIs, you can now create GraphQL APIs that can only be accessed from your Amazon Virtual Private Cloud ("VPC").
* `github.com/aws/aws-sdk-go-v2/service/ec2`: [v1.96.0](service/ec2/CHANGELOG.md#v1960-2023-05-03)
* **Feature**: Adds an SDK paginator for GetNetworkInsightsAccessScopeAnalysisFindings
* `github.com/aws/aws-sdk-go-v2/service/inspector2`: [v1.12.0](service/inspector2/CHANGELOG.md#v1120-2023-05-03)
* **Feature**: This feature provides deep inspection for linux based instance
* `github.com/aws/aws-sdk-go-v2/service/iottwinmaker`: [v1.12.0](service/iottwinmaker/CHANGELOG.md#v1120-2023-05-03)
* **Feature**: This release adds a field for GetScene API to return error code and message from dependency services.
* `github.com/aws/aws-sdk-go-v2/service/networkfirewall`: [v1.27.0](service/networkfirewall/CHANGELOG.md#v1270-2023-05-03)
* **Feature**: AWS Network Firewall now supports policy level HOME_NET variable overrides.
* `github.com/aws/aws-sdk-go-v2/service/opensearch`: [v1.16.0](service/opensearch/CHANGELOG.md#v1160-2023-05-03)
* **Feature**: Amazon OpenSearch Service adds the option to deploy a domain across multiple Availability Zones, with each AZ containing a complete copy of data and with nodes in one AZ acting as a standby. This option provides 99.99% availability and consistent performance in the event of infrastructure failure.
* `github.com/aws/aws-sdk-go-v2/service/wellarchitected`: [v1.20.0](service/wellarchitected/CHANGELOG.md#v1200-2023-05-03)
* **Feature**: This release deepens integration with AWS Service Catalog AppRegistry to improve workload resource discovery.
# Release (2023-05-02)
## Module Highlights
* `github.com/aws/aws-sdk-go-v2/service/appflow`: [v1.29.0](service/appflow/CHANGELOG.md#v1290-2023-05-02)
* **Feature**: This release adds new API to cancel flow executions.
* `github.com/aws/aws-sdk-go-v2/service/connect`: [v1.54.0](service/connect/CHANGELOG.md#v1540-2023-05-02)
* **Feature**: Amazon Connect Service Rules API update: Added OnContactEvaluationSubmit event source to support user configuring evaluation form rules.
* `github.com/aws/aws-sdk-go-v2/service/ecs`: [v1.26.3](service/ecs/CHANGELOG.md#v1263-2023-05-02)
* **Documentation**: Documentation only update to address Amazon ECS tickets.
* `github.com/aws/aws-sdk-go-v2/service/kendra`: [v1.40.0](service/kendra/CHANGELOG.md#v1400-2023-05-02)
* **Feature**: AWS Kendra now supports configuring document fields/attributes via the GetQuerySuggestions API. You can now base query suggestions on the contents of document fields.
* `github.com/aws/aws-sdk-go-v2/service/resiliencehub`: [v1.11.0](service/resiliencehub/CHANGELOG.md#v1110-2023-05-02)
* **Feature**: This release will improve resource level transparency in applications by discovering previously hidden resources.
* `github.com/aws/aws-sdk-go-v2/service/sagemaker`: [v1.76.0](service/sagemaker/CHANGELOG.md#v1760-2023-05-02)
* **Feature**: Amazon Sagemaker Autopilot supports training models with sample weights and additional objective metrics.
# Release (2023-05-01)
## Module Highlights
* `github.com/aws/aws-sdk-go-v2/service/computeoptimizer`: [v1.23.0](service/computeoptimizer/CHANGELOG.md#v1230-2023-05-01)
* **Feature**: support for tag filtering within compute optimizer. ability to filter recommendation results by tag and tag key value pairs. ability to filter by inferred workload type added.
* `github.com/aws/aws-sdk-go-v2/service/kms`: [v1.21.0](service/kms/CHANGELOG.md#v1210-2023-05-01)
* **Feature**: This release makes the NitroEnclave request parameter Recipient and the response field for CiphertextForRecipient available in AWS SDKs. It also adds the regex pattern for CloudHsmClusterId validation.
# Release (2023-04-28)
## Module Highlights
* `github.com/aws/aws-sdk-go-v2/service/appflow`: [v1.28.0](service/appflow/CHANGELOG.md#v1280-2023-04-28)
* **Feature**: Adds Jwt Support for Salesforce Credentials.
* `github.com/aws/aws-sdk-go-v2/service/athena`: [v1.26.0](service/athena/CHANGELOG.md#v1260-2023-04-28)
* **Feature**: You can now use capacity reservations on Amazon Athena to run SQL queries on fully-managed compute capacity.
* `github.com/aws/aws-sdk-go-v2/service/directconnect`: [v1.18.12](service/directconnect/CHANGELOG.md#v11812-2023-04-28)
* **Documentation**: This release corrects the jumbo frames MTU from 9100 to 8500.
* `github.com/aws/aws-sdk-go-v2/service/efs`: [v1.20.0](service/efs/CHANGELOG.md#v1200-2023-04-28)
* **Feature**: This release adds PAUSED and PAUSING state as a returned value for DescribeReplicationConfigurations response.
* `github.com/aws/aws-sdk-go-v2/service/grafana`: [v1.13.0](service/grafana/CHANGELOG.md#v1130-2023-04-28)
* **Feature**: This release adds support for the grafanaVersion parameter in CreateWorkspace.
* `github.com/aws/aws-sdk-go-v2/service/iot`: [v1.37.0](service/iot/CHANGELOG.md#v1370-2023-04-28)
* **Feature**: This release allows AWS IoT Core users to specify a TLS security policy when creating and updating AWS IoT Domain Configurations.
* `github.com/aws/aws-sdk-go-v2/service/rekognition`: [v1.26.0](service/rekognition/CHANGELOG.md#v1260-2023-04-28)
* **Feature**: Added support for aggregating moderation labels by video segment timestamps for Stored Video Content Moderation APIs and added additional information about the job to all Stored Video Get API responses.
* `github.com/aws/aws-sdk-go-v2/service/simspaceweaver`: [v1.2.0](service/simspaceweaver/CHANGELOG.md#v120-2023-04-28)
* **Feature**: Added a new CreateSnapshot API. For the StartSimulation API, SchemaS3Location is now optional, added a new SnapshotS3Location parameter. For the DescribeSimulation API, added SNAPSHOT_IN_PROGRESS simulation state, deprecated SchemaError, added new fields: StartError and SnapshotS3Location.
* `github.com/aws/aws-sdk-go-v2/service/wafv2`: [v1.31.0](service/wafv2/CHANGELOG.md#v1310-2023-04-28)
* **Feature**: You can now associate a web ACL with a Verified Access instance.
* `github.com/aws/aws-sdk-go-v2/service/workspaces`: [v1.28.11](service/workspaces/CHANGELOG.md#v12811-2023-04-28)
* **Documentation**: Added Windows 11 to support Microsoft_Office_2019
# Release (2023-04-27)
## Module Highlights
* `github.com/aws/aws-sdk-go-v2/service/ec2`: [v1.95.0](service/ec2/CHANGELOG.md#v1950-2023-04-27)
* **Feature**: This release adds support for AMD SEV-SNP on EC2 instances.
* `github.com/aws/aws-sdk-go-v2/service/emrcontainers`: [v1.18.0](service/emrcontainers/CHANGELOG.md#v1180-2023-04-27)
* **Feature**: This release adds GetManagedEndpointSessionCredentials, a new API that allows customers to generate an auth token to connect to a managed endpoint, enabling features such as self-hosted Jupyter notebooks for EMR on EKS.
* `github.com/aws/aws-sdk-go-v2/service/guardduty`: [v1.22.0](service/guardduty/CHANGELOG.md#v1220-2023-04-27)
* **Feature**: Added API support to initiate on-demand malware scan on specific resources.
* `github.com/aws/aws-sdk-go-v2/service/iotdeviceadvisor`: [v1.18.0](service/iotdeviceadvisor/CHANGELOG.md#v1180-2023-04-27)
* **Feature**: AWS IoT Core Device Advisor now supports MQTT over WebSocket. With this update, customers can run all three test suites of AWS IoT Core Device Advisor - qualification, custom, and long duration tests - using Signature Version 4 for MQTT over WebSocket.
* `github.com/aws/aws-sdk-go-v2/service/kafka`: [v1.20.0](service/kafka/CHANGELOG.md#v1200-2023-04-27)
* **Feature**: Amazon MSK has added new APIs that allows multi-VPC private connectivity and cluster policy support for Amazon MSK clusters that simplify connectivity and access between your Apache Kafka clients hosted in different VPCs and AWS accounts and your Amazon MSK clusters.
* `github.com/aws/aws-sdk-go-v2/service/lambda`: [v1.34.0](service/lambda/CHANGELOG.md#v1340-2023-04-27)
* **Feature**: Add Java 17 (java17) support to AWS Lambda
* `github.com/aws/aws-sdk-go-v2/service/osis`: [v1.0.1](service/osis/CHANGELOG.md#v101-2023-04-27)
* **Documentation**: Documentation updates for OpenSearch Ingestion
* `github.com/aws/aws-sdk-go-v2/service/qldb`: [v1.15.10](service/qldb/CHANGELOG.md#v11510-2023-04-27)
* **Documentation**: Documentation updates for Amazon QLDB
* `github.com/aws/aws-sdk-go-v2/service/sagemaker`: [v1.75.0](service/sagemaker/CHANGELOG.md#v1750-2023-04-27)
* **Feature**: Added ml.p4d.24xlarge and ml.p4de.24xlarge as supported instances for SageMaker Studio
# Release (2023-04-26)
## Module Highlights
* `github.com/aws/aws-sdk-go-v2/service/osis`: [v1.0.0](service/osis/CHANGELOG.md#v100-2023-04-26)
* **Release**: New AWS service client module
* **Feature**: Initial release for OpenSearch Ingestion
# Release (2023-04-25)
## Module Highlights
* `github.com/aws/aws-sdk-go-v2/service/chimesdkmessaging`: [v1.15.0](service/chimesdkmessaging/CHANGELOG.md#v1150-2023-04-25)
* **Feature**: Remove non actionable field from UpdateChannelReadMarker and DeleteChannelRequest. Add precise exceptions to DeleteChannel and DeleteStreamingConfigurations error cases.
* `github.com/aws/aws-sdk-go-v2/service/connect`: [v1.53.0](service/connect/CHANGELOG.md#v1530-2023-04-25)
* **Feature**: Amazon Connect, Contact Lens Evaluation API release including ability to manage forms and to submit contact evaluations.
* `github.com/aws/aws-sdk-go-v2/service/datasync`: [v1.24.0](service/datasync/CHANGELOG.md#v1240-2023-04-25)
* **Feature**: This release adds 13 new APIs to support AWS DataSync Discovery GA.
* `github.com/aws/aws-sdk-go-v2/service/directoryservice`: [v1.17.0](service/directoryservice/CHANGELOG.md#v1170-2023-04-25)
* **Feature**: New field added in AWS Managed Microsoft AD DescribeSettings response and regex pattern update for UpdateSettings value. Added length validation to RemoteDomainName.
* `github.com/aws/aws-sdk-go-v2/service/pinpoint`: [v1.19.0](service/pinpoint/CHANGELOG.md#v1190-2023-04-25)
* **Feature**: Adds support for journey runs and querying journey execution metrics based on journey runs. Adds execution metrics to campaign activities. Updates docs for Advanced Quiet Time.
# Release (2023-04-24)
## General Highlights

View file

@ -3,4 +3,4 @@
package aws
// goModuleVersion is the tagged release for this module
const goModuleVersion = "1.18.0"
const goModuleVersion = "1.18.1"

Some files were not shown because too many files have changed in this diff Show more