chore (maintain): maintain sqlite full text search plugin

This commit is contained in:
MickaelK
2023-11-22 18:46:46 +11:00
parent 9323c73fa9
commit 253cb8ceba
179 changed files with 111787 additions and 54352 deletions
+1 -1
View File
@@ -12,7 +12,7 @@ require (
github.com/gorilla/websocket v1.4.1
github.com/h2non/bimg v1.1.5
github.com/hirochachacha/go-smb2 v1.1.0
github.com/mattn/go-sqlite3 v2.0.2+incompatible
github.com/mattn/go-sqlite3 v1.14.18
github.com/mickael-kerjean/net v0.0.0-20191120063050-2457c043ba06
github.com/mickael-kerjean/saml v0.0.0-20221221152539-19783715740c
github.com/mitchellh/hashstructure v1.0.0
+2
View File
@@ -148,6 +148,8 @@ github.com/mattermost/xml-roundtrip-validator v0.1.0/go.mod h1:qccnGMcpgwcNaBnxq
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-sqlite3 v1.14.18 h1:JL0eqdCOq6DJVNPSvArO/bIV9/P7fbGrV00LZHc+5aI=
github.com/mattn/go-sqlite3 v1.14.18/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/mattn/go-sqlite3 v2.0.2+incompatible h1:qzw9c2GNT8UFrgWNDhCTqRqYUSmu/Dav/9Z58LGpk7U=
github.com/mattn/go-sqlite3 v2.0.2+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mickael-kerjean/net v0.0.0-20191120063050-2457c043ba06 h1:427Mpu5edwuFuDBdqOm6EgRohYqZ9Oomd+87ryW2Uls=
+9 -3
View File
@@ -13,7 +13,10 @@ var (
CYCLE_TIME func() int
INDEXING_EXT func() string
MAX_INDEXING_FSIZE func() int
INDEXING_EXCLUSION = []string{"/node_modules/", "/bower_components/", "/.cache/", "/.npm/", "/.git/"}
INDEXING_EXCLUSION = []string{
"/node_modules/", "/bower_components/",
"/.cache/", "/.npm/", "/.git/",
}
)
func init() {
@@ -24,7 +27,10 @@ func init() {
}
f.Name = "enable"
f.Type = "enable"
f.Target = []string{"process_max", "process_par", "reindex_time", "cycle_time", "max_size", "indexer_ext"}
f.Target = []string{
"process_max", "process_par", "reindex_time",
"cycle_time", "max_size", "indexer_ext",
}
f.Description = "Enable/Disable full text search"
f.Placeholder = "Default: false"
f.Default = false
@@ -114,7 +120,7 @@ func init() {
}
f.Id = "indexer_ext"
f.Name = "indexer_ext"
f.Type = "string"
f.Type = "text"
f.Description = "File extension we want to see indexed"
f.Placeholder = "Default: org,txt,docx,pdf,md,form"
f.Default = "org,txt,docx,pdf,md,form"
+16 -16
View File
@@ -75,39 +75,39 @@ func (this SqliteSearch) Query(app App, path string, keyword string) ([]IFile, e
type SearchHint struct{}
func (this SearchHint) Ls(ctx App, path string) error {
go SProc.HintLs(&ctx, path)
func (this SearchHint) Ls(ctx *App, path string) error {
go SProc.HintLs(ctx, path)
return nil
}
func (this SearchHint) Cat(ctx App, path string) error {
go SProc.HintLs(&ctx, filepath.Dir(path)+"/")
func (this SearchHint) Cat(ctx *App, path string) error {
go SProc.HintLs(ctx, filepath.Dir(path)+"/")
return nil
}
func (this SearchHint) Mkdir(ctx App, path string) error {
go SProc.HintLs(&ctx, filepath.Dir(path)+"/")
func (this SearchHint) Mkdir(ctx *App, path string) error {
go SProc.HintLs(ctx, filepath.Dir(path)+"/")
return nil
}
func (this SearchHint) Rm(ctx App, path string) error {
go SProc.HintRm(&ctx, path)
func (this SearchHint) Rm(ctx *App, path string) error {
go SProc.HintRm(ctx, path)
return nil
}
func (this SearchHint) Mv(ctx App, from string, to string) error {
go SProc.HintRm(&ctx, filepath.Dir(from)+"/")
go SProc.HintLs(&ctx, filepath.Dir(to)+"/")
func (this SearchHint) Mv(ctx *App, from string, to string) error {
go SProc.HintRm(ctx, filepath.Dir(from)+"/")
go SProc.HintLs(ctx, filepath.Dir(to)+"/")
return nil
}
func (this SearchHint) Save(ctx App, path string) error {
go SProc.HintLs(&ctx, filepath.Dir(path)+"/")
go SProc.HintFile(&ctx, path)
func (this SearchHint) Save(ctx *App, path string) error {
go SProc.HintLs(ctx, filepath.Dir(path)+"/")
go SProc.HintFile(ctx, path)
return nil
}
func (this SearchHint) Touch(ctx App, path string) error {
go SProc.HintLs(&ctx, filepath.Dir(path)+"/")
func (this SearchHint) Touch(ctx *App, path string) error {
go SProc.HintLs(ctx, filepath.Dir(path)+"/")
return nil
}
+315
View File
@@ -0,0 +1,315 @@
// Copyright 2016 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.
// Package iam supports the resource-specific operations of Google Cloud
// IAM (Identity and Access Management) for the Google Cloud Libraries.
// See https://cloud.google.com/iam for more about IAM.
//
// Users of the Google Cloud Libraries will typically not use this package
// directly. Instead they will begin with some resource that supports IAM, like
// a pubsub topic, and call its IAM method to get a Handle for that resource.
package iam
import (
"context"
"fmt"
"time"
gax "github.com/googleapis/gax-go/v2"
pb "google.golang.org/genproto/googleapis/iam/v1"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/metadata"
)
// client abstracts the IAMPolicy API to allow multiple implementations.
type client interface {
Get(ctx context.Context, resource string) (*pb.Policy, error)
Set(ctx context.Context, resource string, p *pb.Policy) error
Test(ctx context.Context, resource string, perms []string) ([]string, error)
}
// grpcClient implements client for the standard gRPC-based IAMPolicy service.
type grpcClient struct {
c pb.IAMPolicyClient
}
var withRetry = gax.WithRetry(func() gax.Retryer {
return gax.OnCodes([]codes.Code{
codes.DeadlineExceeded,
codes.Unavailable,
}, gax.Backoff{
Initial: 100 * time.Millisecond,
Max: 60 * time.Second,
Multiplier: 1.3,
})
})
func (g *grpcClient) Get(ctx context.Context, resource string) (*pb.Policy, error) {
var proto *pb.Policy
md := metadata.Pairs("x-goog-request-params", fmt.Sprintf("%s=%v", "resource", resource))
ctx = insertMetadata(ctx, md)
err := gax.Invoke(ctx, func(ctx context.Context, _ gax.CallSettings) error {
var err error
proto, err = g.c.GetIamPolicy(ctx, &pb.GetIamPolicyRequest{Resource: resource})
return err
}, withRetry)
if err != nil {
return nil, err
}
return proto, nil
}
func (g *grpcClient) Set(ctx context.Context, resource string, p *pb.Policy) error {
md := metadata.Pairs("x-goog-request-params", fmt.Sprintf("%s=%v", "resource", resource))
ctx = insertMetadata(ctx, md)
return gax.Invoke(ctx, func(ctx context.Context, _ gax.CallSettings) error {
_, err := g.c.SetIamPolicy(ctx, &pb.SetIamPolicyRequest{
Resource: resource,
Policy: p,
})
return err
}, withRetry)
}
func (g *grpcClient) Test(ctx context.Context, resource string, perms []string) ([]string, error) {
var res *pb.TestIamPermissionsResponse
md := metadata.Pairs("x-goog-request-params", fmt.Sprintf("%s=%v", "resource", resource))
ctx = insertMetadata(ctx, md)
err := gax.Invoke(ctx, func(ctx context.Context, _ gax.CallSettings) error {
var err error
res, err = g.c.TestIamPermissions(ctx, &pb.TestIamPermissionsRequest{
Resource: resource,
Permissions: perms,
})
return err
}, withRetry)
if err != nil {
return nil, err
}
return res.Permissions, nil
}
// A Handle provides IAM operations for a resource.
type Handle struct {
c client
resource string
}
// InternalNewHandle is for use by the Google Cloud Libraries only.
//
// InternalNewHandle returns a Handle for resource.
// The conn parameter refers to a server that must support the IAMPolicy service.
func InternalNewHandle(conn *grpc.ClientConn, resource string) *Handle {
return InternalNewHandleGRPCClient(pb.NewIAMPolicyClient(conn), resource)
}
// InternalNewHandleGRPCClient is for use by the Google Cloud Libraries only.
//
// InternalNewHandleClient returns a Handle for resource using the given
// grpc service that implements IAM as a mixin
func InternalNewHandleGRPCClient(c pb.IAMPolicyClient, resource string) *Handle {
return InternalNewHandleClient(&grpcClient{c: c}, resource)
}
// InternalNewHandleClient is for use by the Google Cloud Libraries only.
//
// InternalNewHandleClient returns a Handle for resource using the given
// client implementation.
func InternalNewHandleClient(c client, resource string) *Handle {
return &Handle{
c: c,
resource: resource,
}
}
// Policy retrieves the IAM policy for the resource.
func (h *Handle) Policy(ctx context.Context) (*Policy, error) {
proto, err := h.c.Get(ctx, h.resource)
if err != nil {
return nil, err
}
return &Policy{InternalProto: proto}, nil
}
// SetPolicy replaces the resource's current policy with the supplied Policy.
//
// If policy was created from a prior call to Get, then the modification will
// only succeed if the policy has not changed since the Get.
func (h *Handle) SetPolicy(ctx context.Context, policy *Policy) error {
return h.c.Set(ctx, h.resource, policy.InternalProto)
}
// TestPermissions returns the subset of permissions that the caller has on the resource.
func (h *Handle) TestPermissions(ctx context.Context, permissions []string) ([]string, error) {
return h.c.Test(ctx, h.resource, permissions)
}
// A RoleName is a name representing a collection of permissions.
type RoleName string
// Common role names.
const (
Owner RoleName = "roles/owner"
Editor RoleName = "roles/editor"
Viewer RoleName = "roles/viewer"
)
const (
// AllUsers is a special member that denotes all users, even unauthenticated ones.
AllUsers = "allUsers"
// AllAuthenticatedUsers is a special member that denotes all authenticated users.
AllAuthenticatedUsers = "allAuthenticatedUsers"
)
// A Policy is a list of Bindings representing roles
// granted to members.
//
// The zero Policy is a valid policy with no bindings.
type Policy struct {
// TODO(jba): when type aliases are available, put Policy into an internal package
// and provide an exported alias here.
// This field is exported for use by the Google Cloud Libraries only.
// It may become unexported in a future release.
InternalProto *pb.Policy
}
// Members returns the list of members with the supplied role.
// The return value should not be modified. Use Add and Remove
// to modify the members of a role.
func (p *Policy) Members(r RoleName) []string {
b := p.binding(r)
if b == nil {
return nil
}
return b.Members
}
// HasRole reports whether member has role r.
func (p *Policy) HasRole(member string, r RoleName) bool {
return memberIndex(member, p.binding(r)) >= 0
}
// Add adds member member to role r if it is not already present.
// A new binding is created if there is no binding for the role.
func (p *Policy) Add(member string, r RoleName) {
b := p.binding(r)
if b == nil {
if p.InternalProto == nil {
p.InternalProto = &pb.Policy{}
}
p.InternalProto.Bindings = append(p.InternalProto.Bindings, &pb.Binding{
Role: string(r),
Members: []string{member},
})
return
}
if memberIndex(member, b) < 0 {
b.Members = append(b.Members, member)
return
}
}
// Remove removes member from role r if it is present.
func (p *Policy) Remove(member string, r RoleName) {
bi := p.bindingIndex(r)
if bi < 0 {
return
}
bindings := p.InternalProto.Bindings
b := bindings[bi]
mi := memberIndex(member, b)
if mi < 0 {
return
}
// Order doesn't matter for bindings or members, so to remove, move the last item
// into the removed spot and shrink the slice.
if len(b.Members) == 1 {
// Remove binding.
last := len(bindings) - 1
bindings[bi] = bindings[last]
bindings[last] = nil
p.InternalProto.Bindings = bindings[:last]
return
}
// Remove member.
// TODO(jba): worry about multiple copies of m?
last := len(b.Members) - 1
b.Members[mi] = b.Members[last]
b.Members[last] = ""
b.Members = b.Members[:last]
}
// Roles returns the names of all the roles that appear in the Policy.
func (p *Policy) Roles() []RoleName {
if p.InternalProto == nil {
return nil
}
var rns []RoleName
for _, b := range p.InternalProto.Bindings {
rns = append(rns, RoleName(b.Role))
}
return rns
}
// binding returns the Binding for the suppied role, or nil if there isn't one.
func (p *Policy) binding(r RoleName) *pb.Binding {
i := p.bindingIndex(r)
if i < 0 {
return nil
}
return p.InternalProto.Bindings[i]
}
func (p *Policy) bindingIndex(r RoleName) int {
if p.InternalProto == nil {
return -1
}
for i, b := range p.InternalProto.Bindings {
if b.Role == string(r) {
return i
}
}
return -1
}
// memberIndex returns the index of m in b's Members, or -1 if not found.
func memberIndex(m string, b *pb.Binding) int {
if b == nil {
return -1
}
for i, mm := range b.Members {
if mm == m {
return i
}
}
return -1
}
// insertMetadata inserts metadata into the given context
func insertMetadata(ctx context.Context, mds ...metadata.MD) context.Context {
out, _ := metadata.FromOutgoingContext(ctx)
out = out.Copy()
for _, md := range mds {
for k, v := range md {
out[k] = append(out[k], v...)
}
}
return metadata.NewOutgoingContext(ctx, out)
}
+54
View File
@@ -0,0 +1,54 @@
// Copyright 2017 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.
package internal
import (
"fmt"
"google.golang.org/api/googleapi"
"google.golang.org/grpc/status"
)
// Annotate prepends msg to the error message in err, attempting
// to preserve other information in err, like an error code.
//
// Annotate panics if err is nil.
//
// Annotate knows about these error types:
// - "google.golang.org/grpc/status".Status
// - "google.golang.org/api/googleapi".Error
// If the error is not one of these types, Annotate behaves
// like
// fmt.Errorf("%s: %v", msg, err)
func Annotate(err error, msg string) error {
if err == nil {
panic("Annotate called with nil")
}
if s, ok := status.FromError(err); ok {
p := s.Proto()
p.Message = msg + ": " + p.Message
return status.ErrorProto(p)
}
if g, ok := err.(*googleapi.Error); ok {
g.Message = msg + ": " + g.Message
return g
}
return fmt.Errorf("%s: %v", msg, err)
}
// Annotatef uses format and args to format a string, then calls Annotate.
func Annotatef(err error, format string, args ...interface{}) error {
return Annotate(err, fmt.Sprintf(format, args...))
}
+108
View File
@@ -0,0 +1,108 @@
// Copyright 2016 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.
// Package optional provides versions of primitive types that can
// be nil. These are useful in methods that update some of an API object's
// fields.
package optional
import (
"fmt"
"strings"
"time"
)
type (
// Bool is either a bool or nil.
Bool interface{}
// String is either a string or nil.
String interface{}
// Int is either an int or nil.
Int interface{}
// Uint is either a uint or nil.
Uint interface{}
// Float64 is either a float64 or nil.
Float64 interface{}
// Duration is either a time.Duration or nil.
Duration interface{}
)
// ToBool returns its argument as a bool.
// It panics if its argument is nil or not a bool.
func ToBool(v Bool) bool {
x, ok := v.(bool)
if !ok {
doPanic("Bool", v)
}
return x
}
// ToString returns its argument as a string.
// It panics if its argument is nil or not a string.
func ToString(v String) string {
x, ok := v.(string)
if !ok {
doPanic("String", v)
}
return x
}
// ToInt returns its argument as an int.
// It panics if its argument is nil or not an int.
func ToInt(v Int) int {
x, ok := v.(int)
if !ok {
doPanic("Int", v)
}
return x
}
// ToUint returns its argument as a uint.
// It panics if its argument is nil or not a uint.
func ToUint(v Uint) uint {
x, ok := v.(uint)
if !ok {
doPanic("Uint", v)
}
return x
}
// ToFloat64 returns its argument as a float64.
// It panics if its argument is nil or not a float64.
func ToFloat64(v Float64) float64 {
x, ok := v.(float64)
if !ok {
doPanic("Float64", v)
}
return x
}
// ToDuration returns its argument as a time.Duration.
// It panics if its argument is nil or not a time.Duration.
func ToDuration(v Duration) time.Duration {
x, ok := v.(time.Duration)
if !ok {
doPanic("Duration", v)
}
return x
}
func doPanic(capType string, v interface{}) {
panic(fmt.Sprintf("optional.%s value should be %s, got %T", capType, strings.ToLower(capType), v))
}
+54
View File
@@ -0,0 +1,54 @@
// Copyright 2016 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.
package internal
import (
"context"
"time"
gax "github.com/googleapis/gax-go/v2"
)
// Retry calls the supplied function f repeatedly according to the provided
// backoff parameters. It returns when one of the following occurs:
// When f's first return value is true, Retry immediately returns with f's second
// return value.
// When the provided context is done, Retry returns with an error that
// includes both ctx.Error() and the last error returned by f.
func Retry(ctx context.Context, bo gax.Backoff, f func() (stop bool, err error)) error {
return retry(ctx, bo, f, gax.Sleep)
}
func retry(ctx context.Context, bo gax.Backoff, f func() (stop bool, err error),
sleep func(context.Context, time.Duration) error) error {
var lastErr error
for {
stop, err := f()
if stop {
return err
}
// Remember the last "real" error from f.
if err != nil && err != context.Canceled && err != context.DeadlineExceeded {
lastErr = err
}
p := bo.Pause()
if cerr := sleep(ctx, p); cerr != nil {
if lastErr != nil {
return Annotatef(lastErr, "retry failed with %v; last error", cerr)
}
return cerr
}
}
}
+109
View File
@@ -0,0 +1,109 @@
// Copyright 2018 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.
package trace
import (
"context"
"fmt"
"go.opencensus.io/trace"
"google.golang.org/api/googleapi"
"google.golang.org/genproto/googleapis/rpc/code"
"google.golang.org/grpc/status"
)
// StartSpan adds a span to the trace with the given name.
func StartSpan(ctx context.Context, name string) context.Context {
ctx, _ = trace.StartSpan(ctx, name)
return ctx
}
// EndSpan ends a span with the given error.
func EndSpan(ctx context.Context, err error) {
span := trace.FromContext(ctx)
if err != nil {
span.SetStatus(toStatus(err))
}
span.End()
}
// toStatus interrogates an error and converts it to an appropriate
// OpenCensus status.
func toStatus(err error) trace.Status {
if err2, ok := err.(*googleapi.Error); ok {
return trace.Status{Code: httpStatusCodeToOCCode(err2.Code), Message: err2.Message}
} else if s, ok := status.FromError(err); ok {
return trace.Status{Code: int32(s.Code()), Message: s.Message()}
} else {
return trace.Status{Code: int32(code.Code_UNKNOWN), Message: err.Error()}
}
}
// TODO (deklerk): switch to using OpenCensus function when it becomes available.
// Reference: https://github.com/googleapis/googleapis/blob/26b634d2724ac5dd30ae0b0cbfb01f07f2e4050e/google/rpc/code.proto
func httpStatusCodeToOCCode(httpStatusCode int) int32 {
switch httpStatusCode {
case 200:
return int32(code.Code_OK)
case 499:
return int32(code.Code_CANCELLED)
case 500:
return int32(code.Code_UNKNOWN) // Could also be Code_INTERNAL, Code_DATA_LOSS
case 400:
return int32(code.Code_INVALID_ARGUMENT) // Could also be Code_OUT_OF_RANGE
case 504:
return int32(code.Code_DEADLINE_EXCEEDED)
case 404:
return int32(code.Code_NOT_FOUND)
case 409:
return int32(code.Code_ALREADY_EXISTS) // Could also be Code_ABORTED
case 403:
return int32(code.Code_PERMISSION_DENIED)
case 401:
return int32(code.Code_UNAUTHENTICATED)
case 429:
return int32(code.Code_RESOURCE_EXHAUSTED)
case 501:
return int32(code.Code_UNIMPLEMENTED)
case 503:
return int32(code.Code_UNAVAILABLE)
default:
return int32(code.Code_UNKNOWN)
}
}
// TODO: (odeke-em): perhaps just pass around spans due to the cost
// incurred from using trace.FromContext(ctx) yet we could avoid
// throwing away the work done by ctx, span := trace.StartSpan.
func TracePrintf(ctx context.Context, attrMap map[string]interface{}, format string, args ...interface{}) {
var attrs []trace.Attribute
for k, v := range attrMap {
var a trace.Attribute
switch v := v.(type) {
case string:
a = trace.StringAttribute(k, v)
case bool:
a = trace.BoolAttribute(k, v)
case int:
a = trace.Int64Attribute(k, int64(v))
case int64:
a = trace.Int64Attribute(k, v)
default:
a = trace.StringAttribute(k, fmt.Sprintf("%#v", v))
}
attrs = append(attrs, a)
}
trace.FromContext(ctx).Annotatef(attrs, format, args...)
}
+6
View File
@@ -0,0 +1,6 @@
#!/bin/bash
today=$(date +%Y%m%d)
sed -i -r -e 's/const Repo = "([0-9]{8})"/const Repo = "'$today'"/' $GOFILE
+71
View File
@@ -0,0 +1,71 @@
// Copyright 2016 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.
//go:generate ./update_version.sh
// Package version contains version information for Google Cloud Client
// Libraries for Go, as reported in request headers.
package version
import (
"runtime"
"strings"
"unicode"
)
// Repo is the current version of the client libraries in this
// repo. It should be a date in YYYYMMDD format.
const Repo = "20180226"
// Go returns the Go runtime version. The returned string
// has no whitespace.
func Go() string {
return goVersion
}
var goVersion = goVer(runtime.Version())
const develPrefix = "devel +"
func goVer(s string) string {
if strings.HasPrefix(s, develPrefix) {
s = s[len(develPrefix):]
if p := strings.IndexFunc(s, unicode.IsSpace); p >= 0 {
s = s[:p]
}
return s
}
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 ""
}
func notSemverRune(r rune) bool {
return !strings.ContainsRune("0123456789.", r)
}
+335
View File
@@ -0,0 +1,335 @@
// Copyright 2014 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.
package storage
import (
"context"
"net/http"
"reflect"
"cloud.google.com/go/internal/trace"
"google.golang.org/api/googleapi"
raw "google.golang.org/api/storage/v1"
)
// ACLRole is the level of access to grant.
type ACLRole string
const (
RoleOwner ACLRole = "OWNER"
RoleReader ACLRole = "READER"
RoleWriter ACLRole = "WRITER"
)
// ACLEntity refers to a user or group.
// They are sometimes referred to as grantees.
//
// It could be in the form of:
// "user-<userId>", "user-<email>", "group-<groupId>", "group-<email>",
// "domain-<domain>" and "project-team-<projectId>".
//
// Or one of the predefined constants: AllUsers, AllAuthenticatedUsers.
type ACLEntity string
const (
AllUsers ACLEntity = "allUsers"
AllAuthenticatedUsers ACLEntity = "allAuthenticatedUsers"
)
// ACLRule represents a grant for a role to an entity (user, group or team) for a
// Google Cloud Storage object or bucket.
type ACLRule struct {
Entity ACLEntity
EntityID string
Role ACLRole
Domain string
Email string
ProjectTeam *ProjectTeam
}
// ProjectTeam is the project team associated with the entity, if any.
type ProjectTeam struct {
ProjectNumber string
Team string
}
// ACLHandle provides operations on an access control list for a Google Cloud Storage bucket or object.
type ACLHandle struct {
c *Client
bucket string
object string
isDefault bool
userProject string // for requester-pays buckets
}
// Delete permanently deletes the ACL entry for the given entity.
func (a *ACLHandle) Delete(ctx context.Context, entity ACLEntity) (err error) {
ctx = trace.StartSpan(ctx, "cloud.google.com/go/storage.ACL.Delete")
defer func() { trace.EndSpan(ctx, err) }()
if a.object != "" {
return a.objectDelete(ctx, entity)
}
if a.isDefault {
return a.bucketDefaultDelete(ctx, entity)
}
return a.bucketDelete(ctx, entity)
}
// Set sets the role for the given entity.
func (a *ACLHandle) Set(ctx context.Context, entity ACLEntity, role ACLRole) (err error) {
ctx = trace.StartSpan(ctx, "cloud.google.com/go/storage.ACL.Set")
defer func() { trace.EndSpan(ctx, err) }()
if a.object != "" {
return a.objectSet(ctx, entity, role, false)
}
if a.isDefault {
return a.objectSet(ctx, entity, role, true)
}
return a.bucketSet(ctx, entity, role)
}
// List retrieves ACL entries.
func (a *ACLHandle) List(ctx context.Context) (rules []ACLRule, err error) {
ctx = trace.StartSpan(ctx, "cloud.google.com/go/storage.ACL.List")
defer func() { trace.EndSpan(ctx, err) }()
if a.object != "" {
return a.objectList(ctx)
}
if a.isDefault {
return a.bucketDefaultList(ctx)
}
return a.bucketList(ctx)
}
func (a *ACLHandle) bucketDefaultList(ctx context.Context) ([]ACLRule, error) {
var acls *raw.ObjectAccessControls
var err error
err = runWithRetry(ctx, func() error {
req := a.c.raw.DefaultObjectAccessControls.List(a.bucket)
a.configureCall(ctx, req)
acls, err = req.Do()
return err
})
if err != nil {
return nil, err
}
return toObjectACLRules(acls.Items), nil
}
func (a *ACLHandle) bucketDefaultDelete(ctx context.Context, entity ACLEntity) error {
return runWithRetry(ctx, func() error {
req := a.c.raw.DefaultObjectAccessControls.Delete(a.bucket, string(entity))
a.configureCall(ctx, req)
return req.Do()
})
}
func (a *ACLHandle) bucketList(ctx context.Context) ([]ACLRule, error) {
var acls *raw.BucketAccessControls
var err error
err = runWithRetry(ctx, func() error {
req := a.c.raw.BucketAccessControls.List(a.bucket)
a.configureCall(ctx, req)
acls, err = req.Do()
return err
})
if err != nil {
return nil, err
}
return toBucketACLRules(acls.Items), nil
}
func (a *ACLHandle) bucketSet(ctx context.Context, entity ACLEntity, role ACLRole) error {
acl := &raw.BucketAccessControl{
Bucket: a.bucket,
Entity: string(entity),
Role: string(role),
}
err := runWithRetry(ctx, func() error {
req := a.c.raw.BucketAccessControls.Update(a.bucket, string(entity), acl)
a.configureCall(ctx, req)
_, err := req.Do()
return err
})
if err != nil {
return err
}
return nil
}
func (a *ACLHandle) bucketDelete(ctx context.Context, entity ACLEntity) error {
return runWithRetry(ctx, func() error {
req := a.c.raw.BucketAccessControls.Delete(a.bucket, string(entity))
a.configureCall(ctx, req)
return req.Do()
})
}
func (a *ACLHandle) objectList(ctx context.Context) ([]ACLRule, error) {
var acls *raw.ObjectAccessControls
var err error
err = runWithRetry(ctx, func() error {
req := a.c.raw.ObjectAccessControls.List(a.bucket, a.object)
a.configureCall(ctx, req)
acls, err = req.Do()
return err
})
if err != nil {
return nil, err
}
return toObjectACLRules(acls.Items), nil
}
func (a *ACLHandle) objectSet(ctx context.Context, entity ACLEntity, role ACLRole, isBucketDefault bool) error {
type setRequest interface {
Do(opts ...googleapi.CallOption) (*raw.ObjectAccessControl, error)
Header() http.Header
}
acl := &raw.ObjectAccessControl{
Bucket: a.bucket,
Entity: string(entity),
Role: string(role),
}
var req setRequest
if isBucketDefault {
req = a.c.raw.DefaultObjectAccessControls.Update(a.bucket, string(entity), acl)
} else {
req = a.c.raw.ObjectAccessControls.Update(a.bucket, a.object, string(entity), acl)
}
a.configureCall(ctx, req)
return runWithRetry(ctx, func() error {
_, err := req.Do()
return err
})
}
func (a *ACLHandle) objectDelete(ctx context.Context, entity ACLEntity) error {
return runWithRetry(ctx, func() error {
req := a.c.raw.ObjectAccessControls.Delete(a.bucket, a.object, string(entity))
a.configureCall(ctx, req)
return req.Do()
})
}
func (a *ACLHandle) configureCall(ctx context.Context, call interface{ Header() http.Header }) {
vc := reflect.ValueOf(call)
vc.MethodByName("Context").Call([]reflect.Value{reflect.ValueOf(ctx)})
if a.userProject != "" {
vc.MethodByName("UserProject").Call([]reflect.Value{reflect.ValueOf(a.userProject)})
}
setClientHeader(call.Header())
}
func toObjectACLRules(items []*raw.ObjectAccessControl) []ACLRule {
var rs []ACLRule
for _, item := range items {
rs = append(rs, toObjectACLRule(item))
}
return rs
}
func toBucketACLRules(items []*raw.BucketAccessControl) []ACLRule {
var rs []ACLRule
for _, item := range items {
rs = append(rs, toBucketACLRule(item))
}
return rs
}
func toObjectACLRule(a *raw.ObjectAccessControl) ACLRule {
return ACLRule{
Entity: ACLEntity(a.Entity),
EntityID: a.EntityId,
Role: ACLRole(a.Role),
Domain: a.Domain,
Email: a.Email,
ProjectTeam: toObjectProjectTeam(a.ProjectTeam),
}
}
func toBucketACLRule(a *raw.BucketAccessControl) ACLRule {
return ACLRule{
Entity: ACLEntity(a.Entity),
EntityID: a.EntityId,
Role: ACLRole(a.Role),
Domain: a.Domain,
Email: a.Email,
ProjectTeam: toBucketProjectTeam(a.ProjectTeam),
}
}
func toRawObjectACL(rules []ACLRule) []*raw.ObjectAccessControl {
if len(rules) == 0 {
return nil
}
r := make([]*raw.ObjectAccessControl, 0, len(rules))
for _, rule := range rules {
r = append(r, rule.toRawObjectAccessControl("")) // bucket name unnecessary
}
return r
}
func toRawBucketACL(rules []ACLRule) []*raw.BucketAccessControl {
if len(rules) == 0 {
return nil
}
r := make([]*raw.BucketAccessControl, 0, len(rules))
for _, rule := range rules {
r = append(r, rule.toRawBucketAccessControl("")) // bucket name unnecessary
}
return r
}
func (r ACLRule) toRawBucketAccessControl(bucket string) *raw.BucketAccessControl {
return &raw.BucketAccessControl{
Bucket: bucket,
Entity: string(r.Entity),
Role: string(r.Role),
// The other fields are not settable.
}
}
func (r ACLRule) toRawObjectAccessControl(bucket string) *raw.ObjectAccessControl {
return &raw.ObjectAccessControl{
Bucket: bucket,
Entity: string(r.Entity),
Role: string(r.Role),
// The other fields are not settable.
}
}
func toBucketProjectTeam(p *raw.BucketAccessControlProjectTeam) *ProjectTeam {
if p == nil {
return nil
}
return &ProjectTeam{
ProjectNumber: p.ProjectNumber,
Team: p.Team,
}
}
func toObjectProjectTeam(p *raw.ObjectAccessControlProjectTeam) *ProjectTeam {
if p == nil {
return nil
}
return &ProjectTeam{
ProjectNumber: p.ProjectNumber,
Team: p.Team,
}
}
+1186
View File
File diff suppressed because it is too large Load Diff
+228
View File
@@ -0,0 +1,228 @@
// Copyright 2016 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.
package storage
import (
"context"
"errors"
"fmt"
"cloud.google.com/go/internal/trace"
raw "google.golang.org/api/storage/v1"
)
// CopierFrom creates a Copier that can copy src to dst.
// You can immediately call Run on the returned Copier, or
// you can configure it first.
//
// For Requester Pays buckets, the user project of dst is billed, unless it is empty,
// in which case the user project of src is billed.
func (dst *ObjectHandle) CopierFrom(src *ObjectHandle) *Copier {
return &Copier{dst: dst, src: src}
}
// A Copier copies a source object to a destination.
type Copier struct {
// ObjectAttrs are optional attributes to set on the destination object.
// Any attributes must be initialized before any calls on the Copier. Nil
// or zero-valued attributes are ignored.
ObjectAttrs
// RewriteToken can be set before calling Run to resume a copy
// operation. After Run returns a non-nil error, RewriteToken will
// have been updated to contain the value needed to resume the copy.
RewriteToken string
// ProgressFunc can be used to monitor the progress of a multi-RPC copy
// operation. If ProgressFunc is not nil and copying requires multiple
// calls to the underlying service (see
// https://cloud.google.com/storage/docs/json_api/v1/objects/rewrite), then
// ProgressFunc will be invoked after each call with the number of bytes of
// content copied so far and the total size in bytes of the source object.
//
// ProgressFunc is intended to make upload progress available to the
// application. For example, the implementation of ProgressFunc may update
// a progress bar in the application's UI, or log the result of
// float64(copiedBytes)/float64(totalBytes).
//
// ProgressFunc should return quickly without blocking.
ProgressFunc func(copiedBytes, totalBytes uint64)
// The Cloud KMS key, in the form projects/P/locations/L/keyRings/R/cryptoKeys/K,
// that will be used to encrypt the object. Overrides the object's KMSKeyName, if
// any.
//
// Providing both a DestinationKMSKeyName and a customer-supplied encryption key
// (via ObjectHandle.Key) on the destination object will result in an error when
// Run is called.
DestinationKMSKeyName string
dst, src *ObjectHandle
}
// Run performs the copy.
func (c *Copier) Run(ctx context.Context) (attrs *ObjectAttrs, err error) {
ctx = trace.StartSpan(ctx, "cloud.google.com/go/storage.Copier.Run")
defer func() { trace.EndSpan(ctx, err) }()
if err := c.src.validate(); err != nil {
return nil, err
}
if err := c.dst.validate(); err != nil {
return nil, err
}
if c.DestinationKMSKeyName != "" && c.dst.encryptionKey != nil {
return nil, errors.New("storage: cannot use DestinationKMSKeyName with a customer-supplied encryption key")
}
// Convert destination attributes to raw form, omitting the bucket.
// If the bucket is included but name or content-type aren't, the service
// returns a 400 with "Required" as the only message. Omitting the bucket
// does not cause any problems.
rawObject := c.ObjectAttrs.toRawObject("")
for {
res, err := c.callRewrite(ctx, rawObject)
if err != nil {
return nil, err
}
if c.ProgressFunc != nil {
c.ProgressFunc(uint64(res.TotalBytesRewritten), uint64(res.ObjectSize))
}
if res.Done { // Finished successfully.
return newObject(res.Resource), nil
}
}
}
func (c *Copier) callRewrite(ctx context.Context, rawObj *raw.Object) (*raw.RewriteResponse, error) {
call := c.dst.c.raw.Objects.Rewrite(c.src.bucket, c.src.object, c.dst.bucket, c.dst.object, rawObj)
call.Context(ctx).Projection("full")
if c.RewriteToken != "" {
call.RewriteToken(c.RewriteToken)
}
if c.DestinationKMSKeyName != "" {
call.DestinationKmsKeyName(c.DestinationKMSKeyName)
}
if c.PredefinedACL != "" {
call.DestinationPredefinedAcl(c.PredefinedACL)
}
if err := applyConds("Copy destination", c.dst.gen, c.dst.conds, call); err != nil {
return nil, err
}
if c.dst.userProject != "" {
call.UserProject(c.dst.userProject)
} else if c.src.userProject != "" {
call.UserProject(c.src.userProject)
}
if err := applySourceConds(c.src.gen, c.src.conds, call); err != nil {
return nil, err
}
if err := setEncryptionHeaders(call.Header(), c.dst.encryptionKey, false); err != nil {
return nil, err
}
if err := setEncryptionHeaders(call.Header(), c.src.encryptionKey, true); err != nil {
return nil, err
}
var res *raw.RewriteResponse
var err error
setClientHeader(call.Header())
err = runWithRetry(ctx, func() error { res, err = call.Do(); return err })
if err != nil {
return nil, err
}
c.RewriteToken = res.RewriteToken
return res, nil
}
// ComposerFrom creates a Composer that can compose srcs into dst.
// You can immediately call Run on the returned Composer, or you can
// configure it first.
//
// The encryption key for the destination object will be used to decrypt all
// source objects and encrypt the destination object. It is an error
// to specify an encryption key for any of the source objects.
func (dst *ObjectHandle) ComposerFrom(srcs ...*ObjectHandle) *Composer {
return &Composer{dst: dst, srcs: srcs}
}
// A Composer composes source objects into a destination object.
//
// For Requester Pays buckets, the user project of dst is billed.
type Composer struct {
// ObjectAttrs are optional attributes to set on the destination object.
// Any attributes must be initialized before any calls on the Composer. Nil
// or zero-valued attributes are ignored.
ObjectAttrs
dst *ObjectHandle
srcs []*ObjectHandle
}
// Run performs the compose operation.
func (c *Composer) Run(ctx context.Context) (attrs *ObjectAttrs, err error) {
ctx = trace.StartSpan(ctx, "cloud.google.com/go/storage.Composer.Run")
defer func() { trace.EndSpan(ctx, err) }()
if err := c.dst.validate(); err != nil {
return nil, err
}
if len(c.srcs) == 0 {
return nil, errors.New("storage: at least one source object must be specified")
}
req := &raw.ComposeRequest{}
// Compose requires a non-empty Destination, so we always set it,
// even if the caller-provided ObjectAttrs is the zero value.
req.Destination = c.ObjectAttrs.toRawObject(c.dst.bucket)
for _, src := range c.srcs {
if err := src.validate(); err != nil {
return nil, err
}
if src.bucket != c.dst.bucket {
return nil, fmt.Errorf("storage: all source objects must be in bucket %q, found %q", c.dst.bucket, src.bucket)
}
if src.encryptionKey != nil {
return nil, fmt.Errorf("storage: compose source %s.%s must not have encryption key", src.bucket, src.object)
}
srcObj := &raw.ComposeRequestSourceObjects{
Name: src.object,
}
if err := applyConds("ComposeFrom source", src.gen, src.conds, composeSourceObj{srcObj}); err != nil {
return nil, err
}
req.SourceObjects = append(req.SourceObjects, srcObj)
}
call := c.dst.c.raw.Objects.Compose(c.dst.bucket, c.dst.object, req).Context(ctx)
if err := applyConds("ComposeFrom destination", c.dst.gen, c.dst.conds, call); err != nil {
return nil, err
}
if c.dst.userProject != "" {
call.UserProject(c.dst.userProject)
}
if c.PredefinedACL != "" {
call.DestinationPredefinedAcl(c.PredefinedACL)
}
if err := setEncryptionHeaders(call.Header(), c.dst.encryptionKey, false); err != nil {
return nil, err
}
var obj *raw.Object
setClientHeader(call.Header())
err = runWithRetry(ctx, func() error { obj, err = call.Do(); return err })
if err != nil {
return nil, err
}
return newObject(obj), nil
}
+176
View File
@@ -0,0 +1,176 @@
// Copyright 2016 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.
/*
Package storage provides an easy way to work with Google Cloud Storage.
Google Cloud Storage stores data in named objects, which are grouped into buckets.
More information about Google Cloud Storage is available at
https://cloud.google.com/storage/docs.
See https://godoc.org/cloud.google.com/go for authentication, timeouts,
connection pooling and similar aspects of this package.
All of the methods of this package use exponential backoff to retry calls that fail
with certain errors, as described in
https://cloud.google.com/storage/docs/exponential-backoff. Retrying continues
indefinitely unless the controlling context is canceled or the client is closed. See
context.WithTimeout and context.WithCancel.
Creating a Client
To start working with this package, create a client:
ctx := context.Background()
client, err := storage.NewClient(ctx)
if err != nil {
// TODO: Handle error.
}
The client will use your default application credentials.
If you only wish to access public data, you can create
an unauthenticated client with
client, err := storage.NewClient(ctx, option.WithoutAuthentication())
Buckets
A Google Cloud Storage bucket is a collection of objects. To work with a
bucket, make a bucket handle:
bkt := client.Bucket(bucketName)
A handle is a reference to a bucket. You can have a handle even if the
bucket doesn't exist yet. To create a bucket in Google Cloud Storage,
call Create on the handle:
if err := bkt.Create(ctx, projectID, nil); err != nil {
// TODO: Handle error.
}
Note that although buckets are associated with projects, bucket names are
global across all projects.
Each bucket has associated metadata, represented in this package by
BucketAttrs. The third argument to BucketHandle.Create allows you to set
the initial BucketAttrs of a bucket. To retrieve a bucket's attributes, use
Attrs:
attrs, err := bkt.Attrs(ctx)
if err != nil {
// TODO: Handle error.
}
fmt.Printf("bucket %s, created at %s, is located in %s with storage class %s\n",
attrs.Name, attrs.Created, attrs.Location, attrs.StorageClass)
Objects
An object holds arbitrary data as a sequence of bytes, like a file. You
refer to objects using a handle, just as with buckets, but unlike buckets
you don't explicitly create an object. Instead, the first time you write
to an object it will be created. You can use the standard Go io.Reader
and io.Writer interfaces to read and write object data:
obj := bkt.Object("data")
// Write something to obj.
// w implements io.Writer.
w := obj.NewWriter(ctx)
// Write some text to obj. This will either create the object or overwrite whatever is there already.
if _, err := fmt.Fprintf(w, "This object contains text.\n"); err != nil {
// TODO: Handle error.
}
// Close, just like writing a file.
if err := w.Close(); err != nil {
// TODO: Handle error.
}
// Read it back.
r, err := obj.NewReader(ctx)
if err != nil {
// TODO: Handle error.
}
defer r.Close()
if _, err := io.Copy(os.Stdout, r); err != nil {
// TODO: Handle error.
}
// Prints "This object contains text."
Objects also have attributes, which you can fetch with Attrs:
objAttrs, err := obj.Attrs(ctx)
if err != nil {
// TODO: Handle error.
}
fmt.Printf("object %s has size %d and can be read using %s\n",
objAttrs.Name, objAttrs.Size, objAttrs.MediaLink)
ACLs
Both objects and buckets have ACLs (Access Control Lists). An ACL is a list of
ACLRules, each of which specifies the role of a user, group or project. ACLs
are suitable for fine-grained control, but you may prefer using IAM to control
access at the project level (see
https://cloud.google.com/storage/docs/access-control/iam).
To list the ACLs of a bucket or object, obtain an ACLHandle and call its List method:
acls, err := obj.ACL().List(ctx)
if err != nil {
// TODO: Handle error.
}
for _, rule := range acls {
fmt.Printf("%s has role %s\n", rule.Entity, rule.Role)
}
You can also set and delete ACLs.
Conditions
Every object has a generation and a metageneration. The generation changes
whenever the content changes, and the metageneration changes whenever the
metadata changes. Conditions let you check these values before an operation;
the operation only executes if the conditions match. You can use conditions to
prevent race conditions in read-modify-write operations.
For example, say you've read an object's metadata into objAttrs. Now
you want to write to that object, but only if its contents haven't changed
since you read it. Here is how to express that:
w = obj.If(storage.Conditions{GenerationMatch: objAttrs.Generation}).NewWriter(ctx)
// Proceed with writing as above.
Signed URLs
You can obtain a URL that lets anyone read or write an object for a limited time.
You don't need to create a client to do this. See the documentation of
SignedURL for details.
url, err := storage.SignedURL(bucketName, "shared-object", opts)
if err != nil {
// TODO: Handle error.
}
fmt.Println(url)
Errors
Errors returned by this client are often of the type [`googleapi.Error`](https://godoc.org/google.golang.org/api/googleapi#Error).
These errors can be introspected for more information by type asserting to the richer `googleapi.Error` type. For example:
if e, ok := err.(*googleapi.Error); ok {
if e.Code == 409 { ... }
}
*/
package storage // import "cloud.google.com/go/storage"
@@ -1,10 +1,10 @@
// Copyright 2013 Google Inc. All rights reserved.
// Copyright 2017 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
// 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,
@@ -12,14 +12,21 @@
// See the License for the specific language governing permissions and
// limitations under the License.
// Package pretty pretty-prints Go structures.
//
// This package uses reflection to examine a Go value and can
// print out in a nice, aligned fashion. It supports three
// modes (normal, compact, and extended) for advanced use.
//
// See the Reflect and Print examples for what the output looks like.
package pretty
// +build go1.10
// TODO:
// - Catch cycles
package storage
import "google.golang.org/api/googleapi"
func shouldRetry(err error) bool {
switch e := err.(type) {
case *googleapi.Error:
// Retry on 429 and 5xx, according to
// https://cloud.google.com/storage/docs/exponential-backoff.
return e.Code == 429 || (e.Code >= 500 && e.Code < 600)
case interface{ Temporary() bool }:
return e.Temporary()
default:
return false
}
}
+130
View File
@@ -0,0 +1,130 @@
// Copyright 2017 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.
package storage
import (
"context"
"cloud.google.com/go/iam"
"cloud.google.com/go/internal/trace"
raw "google.golang.org/api/storage/v1"
iampb "google.golang.org/genproto/googleapis/iam/v1"
)
// IAM provides access to IAM access control for the bucket.
func (b *BucketHandle) IAM() *iam.Handle {
return iam.InternalNewHandleClient(&iamClient{
raw: b.c.raw,
userProject: b.userProject,
}, b.name)
}
// iamClient implements the iam.client interface.
type iamClient struct {
raw *raw.Service
userProject string
}
func (c *iamClient) Get(ctx context.Context, resource string) (p *iampb.Policy, err error) {
ctx = trace.StartSpan(ctx, "cloud.google.com/go/storage.IAM.Get")
defer func() { trace.EndSpan(ctx, err) }()
call := c.raw.Buckets.GetIamPolicy(resource)
setClientHeader(call.Header())
if c.userProject != "" {
call.UserProject(c.userProject)
}
var rp *raw.Policy
err = runWithRetry(ctx, func() error {
rp, err = call.Context(ctx).Do()
return err
})
if err != nil {
return nil, err
}
return iamFromStoragePolicy(rp), nil
}
func (c *iamClient) Set(ctx context.Context, resource string, p *iampb.Policy) (err error) {
ctx = trace.StartSpan(ctx, "cloud.google.com/go/storage.IAM.Set")
defer func() { trace.EndSpan(ctx, err) }()
rp := iamToStoragePolicy(p)
call := c.raw.Buckets.SetIamPolicy(resource, rp)
setClientHeader(call.Header())
if c.userProject != "" {
call.UserProject(c.userProject)
}
return runWithRetry(ctx, func() error {
_, err := call.Context(ctx).Do()
return err
})
}
func (c *iamClient) Test(ctx context.Context, resource string, perms []string) (permissions []string, err error) {
ctx = trace.StartSpan(ctx, "cloud.google.com/go/storage.IAM.Test")
defer func() { trace.EndSpan(ctx, err) }()
call := c.raw.Buckets.TestIamPermissions(resource, perms)
setClientHeader(call.Header())
if c.userProject != "" {
call.UserProject(c.userProject)
}
var res *raw.TestIamPermissionsResponse
err = runWithRetry(ctx, func() error {
res, err = call.Context(ctx).Do()
return err
})
if err != nil {
return nil, err
}
return res.Permissions, nil
}
func iamToStoragePolicy(ip *iampb.Policy) *raw.Policy {
return &raw.Policy{
Bindings: iamToStorageBindings(ip.Bindings),
Etag: string(ip.Etag),
}
}
func iamToStorageBindings(ibs []*iampb.Binding) []*raw.PolicyBindings {
var rbs []*raw.PolicyBindings
for _, ib := range ibs {
rbs = append(rbs, &raw.PolicyBindings{
Role: ib.Role,
Members: ib.Members,
})
}
return rbs
}
func iamFromStoragePolicy(rp *raw.Policy) *iampb.Policy {
return &iampb.Policy{
Bindings: iamFromStorageBindings(rp.Bindings),
Etag: []byte(rp.Etag),
}
}
func iamFromStorageBindings(rbs []*raw.PolicyBindings) []*iampb.Binding {
var ibs []*iampb.Binding
for _, rb := range rbs {
ibs = append(ibs, &iampb.Binding{
Role: rb.Role,
Members: rb.Members,
})
}
return ibs
}
+37
View File
@@ -0,0 +1,37 @@
// Copyright 2014 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.
package storage
import (
"context"
"cloud.google.com/go/internal"
gax "github.com/googleapis/gax-go/v2"
)
// runWithRetry calls the function until it returns nil or a non-retryable error, or
// the context is done.
func runWithRetry(ctx context.Context, call func() error) error {
return internal.Retry(ctx, gax.Backoff{}, func() (stop bool, err error) {
err = call()
if err == nil {
return true, nil
}
if shouldRetry(err) {
return false, nil
}
return true, err
})
}
+42
View File
@@ -0,0 +1,42 @@
// Copyright 2017 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.
// +build !go1.10
package storage
import (
"net/url"
"strings"
"google.golang.org/api/googleapi"
)
func shouldRetry(err error) bool {
switch e := err.(type) {
case *googleapi.Error:
// Retry on 429 and 5xx, according to
// https://cloud.google.com/storage/docs/exponential-backoff.
return e.Code == 429 || (e.Code >= 500 && e.Code < 600)
case *url.Error:
// Retry on REFUSED_STREAM.
// Unfortunately the error type is unexported, so we resort to string
// matching.
return strings.Contains(e.Error(), "REFUSED_STREAM")
case interface{ Temporary() bool }:
return e.Temporary()
default:
return false
}
}
+188
View File
@@ -0,0 +1,188 @@
// Copyright 2017 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.
package storage
import (
"context"
"errors"
"fmt"
"regexp"
"cloud.google.com/go/internal/trace"
raw "google.golang.org/api/storage/v1"
)
// A Notification describes how to send Cloud PubSub messages when certain
// events occur in a bucket.
type Notification struct {
//The ID of the notification.
ID string
// The ID of the topic to which this subscription publishes.
TopicID string
// The ID of the project to which the topic belongs.
TopicProjectID string
// Only send notifications about listed event types. If empty, send notifications
// for all event types.
// See https://cloud.google.com/storage/docs/pubsub-notifications#events.
EventTypes []string
// If present, only apply this notification configuration to object names that
// begin with this prefix.
ObjectNamePrefix string
// An optional list of additional attributes to attach to each Cloud PubSub
// message published for this notification subscription.
CustomAttributes map[string]string
// The contents of the message payload.
// See https://cloud.google.com/storage/docs/pubsub-notifications#payload.
PayloadFormat string
}
// Values for Notification.PayloadFormat.
const (
// Send no payload with notification messages.
NoPayload = "NONE"
// Send object metadata as JSON with notification messages.
JSONPayload = "JSON_API_V1"
)
// Values for Notification.EventTypes.
const (
// Event that occurs when an object is successfully created.
ObjectFinalizeEvent = "OBJECT_FINALIZE"
// Event that occurs when the metadata of an existing object changes.
ObjectMetadataUpdateEvent = "OBJECT_METADATA_UPDATE"
// Event that occurs when an object is permanently deleted.
ObjectDeleteEvent = "OBJECT_DELETE"
// Event that occurs when the live version of an object becomes an
// archived version.
ObjectArchiveEvent = "OBJECT_ARCHIVE"
)
func toNotification(rn *raw.Notification) *Notification {
n := &Notification{
ID: rn.Id,
EventTypes: rn.EventTypes,
ObjectNamePrefix: rn.ObjectNamePrefix,
CustomAttributes: rn.CustomAttributes,
PayloadFormat: rn.PayloadFormat,
}
n.TopicProjectID, n.TopicID = parseNotificationTopic(rn.Topic)
return n
}
var topicRE = regexp.MustCompile("^//pubsub.googleapis.com/projects/([^/]+)/topics/([^/]+)")
// parseNotificationTopic extracts the project and topic IDs from from the full
// resource name returned by the service. If the name is malformed, it returns
// "?" for both IDs.
func parseNotificationTopic(nt string) (projectID, topicID string) {
matches := topicRE.FindStringSubmatch(nt)
if matches == nil {
return "?", "?"
}
return matches[1], matches[2]
}
func toRawNotification(n *Notification) *raw.Notification {
return &raw.Notification{
Id: n.ID,
Topic: fmt.Sprintf("//pubsub.googleapis.com/projects/%s/topics/%s",
n.TopicProjectID, n.TopicID),
EventTypes: n.EventTypes,
ObjectNamePrefix: n.ObjectNamePrefix,
CustomAttributes: n.CustomAttributes,
PayloadFormat: string(n.PayloadFormat),
}
}
// AddNotification adds a notification to b. You must set n's TopicProjectID, TopicID
// and PayloadFormat, and must not set its ID. The other fields are all optional. The
// returned Notification's ID can be used to refer to it.
func (b *BucketHandle) AddNotification(ctx context.Context, n *Notification) (ret *Notification, err error) {
ctx = trace.StartSpan(ctx, "cloud.google.com/go/storage.Bucket.AddNotification")
defer func() { trace.EndSpan(ctx, err) }()
if n.ID != "" {
return nil, errors.New("storage: AddNotification: ID must not be set")
}
if n.TopicProjectID == "" {
return nil, errors.New("storage: AddNotification: missing TopicProjectID")
}
if n.TopicID == "" {
return nil, errors.New("storage: AddNotification: missing TopicID")
}
call := b.c.raw.Notifications.Insert(b.name, toRawNotification(n))
setClientHeader(call.Header())
if b.userProject != "" {
call.UserProject(b.userProject)
}
rn, err := call.Context(ctx).Do()
if err != nil {
return nil, err
}
return toNotification(rn), nil
}
// Notifications returns all the Notifications configured for this bucket, as a map
// indexed by notification ID.
func (b *BucketHandle) Notifications(ctx context.Context) (n map[string]*Notification, err error) {
ctx = trace.StartSpan(ctx, "cloud.google.com/go/storage.Bucket.Notifications")
defer func() { trace.EndSpan(ctx, err) }()
call := b.c.raw.Notifications.List(b.name)
setClientHeader(call.Header())
if b.userProject != "" {
call.UserProject(b.userProject)
}
var res *raw.Notifications
err = runWithRetry(ctx, func() error {
res, err = call.Context(ctx).Do()
return err
})
if err != nil {
return nil, err
}
return notificationsToMap(res.Items), nil
}
func notificationsToMap(rns []*raw.Notification) map[string]*Notification {
m := map[string]*Notification{}
for _, rn := range rns {
m[rn.Id] = toNotification(rn)
}
return m
}
// DeleteNotification deletes the notification with the given ID.
func (b *BucketHandle) DeleteNotification(ctx context.Context, id string) (err error) {
ctx = trace.StartSpan(ctx, "cloud.google.com/go/storage.Bucket.DeleteNotification")
defer func() { trace.EndSpan(ctx, err) }()
call := b.c.raw.Notifications.Delete(b.name, id)
setClientHeader(call.Header())
if b.userProject != "" {
call.UserProject(b.userProject)
}
return call.Context(ctx).Do()
}
+385
View File
@@ -0,0 +1,385 @@
// Copyright 2016 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.
package storage
import (
"context"
"errors"
"fmt"
"hash/crc32"
"io"
"io/ioutil"
"net/http"
"net/url"
"reflect"
"strconv"
"strings"
"time"
"cloud.google.com/go/internal/trace"
"google.golang.org/api/googleapi"
)
var crc32cTable = crc32.MakeTable(crc32.Castagnoli)
// ReaderObjectAttrs are attributes about the object being read. These are populated
// during the New call. This struct only holds a subset of object attributes: to
// get the full set of attributes, use ObjectHandle.Attrs.
//
// Each field is read-only.
type ReaderObjectAttrs struct {
// Size is the length of the object's content.
Size int64
// ContentType is the MIME type of the object's content.
ContentType string
// ContentEncoding is the encoding of the object's content.
ContentEncoding string
// CacheControl specifies whether and for how long browser and Internet
// caches are allowed to cache your objects.
CacheControl string
// LastModified is the time that the object was last modified.
LastModified time.Time
// Generation is the generation number of the object's content.
Generation int64
// Metageneration is the version of the metadata for this object at
// this generation. This field is used for preconditions and for
// detecting changes in metadata. A metageneration number is only
// meaningful in the context of a particular generation of a
// particular object.
Metageneration int64
}
// NewReader creates a new Reader to read the contents of the
// object.
// ErrObjectNotExist will be returned if the object is not found.
//
// The caller must call Close on the returned Reader when done reading.
func (o *ObjectHandle) NewReader(ctx context.Context) (*Reader, error) {
return o.NewRangeReader(ctx, 0, -1)
}
// NewRangeReader reads part of an object, reading at most length bytes
// starting at the given offset. If length is negative, the object is read
// until the end.
func (o *ObjectHandle) NewRangeReader(ctx context.Context, offset, length int64) (r *Reader, err error) {
ctx = trace.StartSpan(ctx, "cloud.google.com/go/storage.Object.NewRangeReader")
defer func() { trace.EndSpan(ctx, err) }()
if err := o.validate(); err != nil {
return nil, err
}
if offset < 0 {
return nil, fmt.Errorf("storage: invalid offset %d < 0", offset)
}
if o.conds != nil {
if err := o.conds.validate("NewRangeReader"); err != nil {
return nil, err
}
}
u := &url.URL{
Scheme: "https",
Host: "storage.googleapis.com",
Path: fmt.Sprintf("/%s/%s", o.bucket, o.object),
}
verb := "GET"
if length == 0 {
verb = "HEAD"
}
req, err := http.NewRequest(verb, u.String(), nil)
if err != nil {
return nil, err
}
req = req.WithContext(ctx)
if o.userProject != "" {
req.Header.Set("X-Goog-User-Project", o.userProject)
}
if o.readCompressed {
req.Header.Set("Accept-Encoding", "gzip")
}
if err := setEncryptionHeaders(req.Header, o.encryptionKey, false); err != nil {
return nil, err
}
gen := o.gen
// Define a function that initiates a Read with offset and length, assuming we
// have already read seen bytes.
reopen := func(seen int64) (*http.Response, error) {
start := offset + seen
if length < 0 && start > 0 {
req.Header.Set("Range", fmt.Sprintf("bytes=%d-", start))
} else if length > 0 {
// The end character isn't affected by how many bytes we've seen.
req.Header.Set("Range", fmt.Sprintf("bytes=%d-%d", start, offset+length-1))
}
// We wait to assign conditions here because the generation number can change in between reopen() runs.
req.URL.RawQuery = conditionsQuery(gen, o.conds)
var res *http.Response
err = runWithRetry(ctx, func() error {
res, err = o.c.hc.Do(req)
if err != nil {
return err
}
if res.StatusCode == http.StatusNotFound {
res.Body.Close()
return ErrObjectNotExist
}
if res.StatusCode < 200 || res.StatusCode > 299 {
body, _ := ioutil.ReadAll(res.Body)
res.Body.Close()
return &googleapi.Error{
Code: res.StatusCode,
Header: res.Header,
Body: string(body),
}
}
if start > 0 && length != 0 && res.StatusCode != http.StatusPartialContent {
res.Body.Close()
return errors.New("storage: partial request not satisfied")
}
// If a generation hasn't been specified, and this is the first response we get, let's record the
// generation. In future requests we'll use this generation as a precondition to avoid data races.
if gen < 0 && res.Header.Get("X-Goog-Generation") != "" {
gen64, err := strconv.ParseInt(res.Header.Get("X-Goog-Generation"), 10, 64)
if err != nil {
return err
}
gen = gen64
}
return nil
})
if err != nil {
return nil, err
}
return res, nil
}
res, err := reopen(0)
if err != nil {
return nil, err
}
var (
size int64 // total size of object, even if a range was requested.
checkCRC bool
crc uint32
)
if res.StatusCode == http.StatusPartialContent {
cr := strings.TrimSpace(res.Header.Get("Content-Range"))
if !strings.HasPrefix(cr, "bytes ") || !strings.Contains(cr, "/") {
return nil, fmt.Errorf("storage: invalid Content-Range %q", cr)
}
size, err = strconv.ParseInt(cr[strings.LastIndex(cr, "/")+1:], 10, 64)
if err != nil {
return nil, fmt.Errorf("storage: invalid Content-Range %q", cr)
}
} else {
size = res.ContentLength
// Check the CRC iff all of the following hold:
// - We asked for content (length != 0).
// - We got all the content (status != PartialContent).
// - The server sent a CRC header.
// - The Go http stack did not uncompress the file.
// - We were not served compressed data that was uncompressed on download.
// The problem with the last two cases is that the CRC will not match -- GCS
// computes it on the compressed contents, but we compute it on the
// uncompressed contents.
if length != 0 && !res.Uncompressed && !uncompressedByServer(res) {
crc, checkCRC = parseCRC32c(res)
}
}
remain := res.ContentLength
body := res.Body
if length == 0 {
remain = 0
body.Close()
body = emptyBody
}
var metaGen int64
if res.Header.Get("X-Goog-Generation") != "" {
metaGen, err = strconv.ParseInt(res.Header.Get("X-Goog-Metageneration"), 10, 64)
if err != nil {
return nil, err
}
}
var lm time.Time
if res.Header.Get("Last-Modified") != "" {
lm, err = http.ParseTime(res.Header.Get("Last-Modified"))
if err != nil {
return nil, err
}
}
attrs := ReaderObjectAttrs{
Size: size,
ContentType: res.Header.Get("Content-Type"),
ContentEncoding: res.Header.Get("Content-Encoding"),
CacheControl: res.Header.Get("Cache-Control"),
LastModified: lm,
Generation: gen,
Metageneration: metaGen,
}
return &Reader{
Attrs: attrs,
body: body,
size: size,
remain: remain,
wantCRC: crc,
checkCRC: checkCRC,
reopen: reopen,
}, nil
}
func uncompressedByServer(res *http.Response) bool {
// If the data is stored as gzip but is not encoded as gzip, then it
// was uncompressed by the server.
return res.Header.Get("X-Goog-Stored-Content-Encoding") == "gzip" &&
res.Header.Get("Content-Encoding") != "gzip"
}
func parseCRC32c(res *http.Response) (uint32, bool) {
const prefix = "crc32c="
for _, spec := range res.Header["X-Goog-Hash"] {
if strings.HasPrefix(spec, prefix) {
c, err := decodeUint32(spec[len(prefix):])
if err == nil {
return c, true
}
}
}
return 0, false
}
var emptyBody = ioutil.NopCloser(strings.NewReader(""))
// Reader reads a Cloud Storage object.
// It implements io.Reader.
//
// Typically, a Reader computes the CRC of the downloaded content and compares it to
// the stored CRC, returning an error from Read if there is a mismatch. This integrity check
// is skipped if transcoding occurs. See https://cloud.google.com/storage/docs/transcoding.
type Reader struct {
Attrs ReaderObjectAttrs
body io.ReadCloser
seen, remain, size int64
checkCRC bool // should we check the CRC?
wantCRC uint32 // the CRC32c value the server sent in the header
gotCRC uint32 // running crc
reopen func(seen int64) (*http.Response, error)
}
// Close closes the Reader. It must be called when done reading.
func (r *Reader) Close() error {
return r.body.Close()
}
func (r *Reader) Read(p []byte) (int, error) {
n, err := r.readWithRetry(p)
if r.remain != -1 {
r.remain -= int64(n)
}
if r.checkCRC {
r.gotCRC = crc32.Update(r.gotCRC, crc32cTable, p[:n])
// Check CRC here. It would be natural to check it in Close, but
// everybody defers Close on the assumption that it doesn't return
// anything worth looking at.
if err == io.EOF {
if r.gotCRC != r.wantCRC {
return n, fmt.Errorf("storage: bad CRC on read: got %d, want %d",
r.gotCRC, r.wantCRC)
}
}
}
return n, err
}
func (r *Reader) readWithRetry(p []byte) (int, error) {
n := 0
for len(p[n:]) > 0 {
m, err := r.body.Read(p[n:])
n += m
r.seen += int64(m)
if !shouldRetryRead(err) {
return n, err
}
// Read failed, but we will try again. Send a ranged read request that takes
// into account the number of bytes we've already seen.
res, err := r.reopen(r.seen)
if err != nil {
// reopen already retries
return n, err
}
r.body.Close()
r.body = res.Body
}
return n, nil
}
func shouldRetryRead(err error) bool {
if err == nil {
return false
}
return strings.HasSuffix(err.Error(), "INTERNAL_ERROR") && strings.Contains(reflect.TypeOf(err).String(), "http2")
}
// Size returns the size of the object in bytes.
// The returned value is always the same and is not affected by
// calls to Read or Close.
//
// Deprecated: use Reader.Attrs.Size.
func (r *Reader) Size() int64 {
return r.Attrs.Size
}
// Remain returns the number of bytes left to read, or -1 if unknown.
func (r *Reader) Remain() int64 {
return r.remain
}
// ContentType returns the content type of the object.
//
// Deprecated: use Reader.Attrs.ContentType.
func (r *Reader) ContentType() string {
return r.Attrs.ContentType
}
// ContentEncoding returns the content encoding of the object.
//
// Deprecated: use Reader.Attrs.ContentEncoding.
func (r *Reader) ContentEncoding() string {
return r.Attrs.ContentEncoding
}
// CacheControl returns the cache control of the object.
//
// Deprecated: use Reader.Attrs.CacheControl.
func (r *Reader) CacheControl() string {
return r.Attrs.CacheControl
}
// LastModified returns the value of the Last-Modified header.
//
// Deprecated: use Reader.Attrs.LastModified.
func (r *Reader) LastModified() (time.Time, error) {
return r.Attrs.LastModified, nil
}
+1116
View File
File diff suppressed because it is too large Load Diff
+23841
View File
File diff suppressed because one or more lines are too long
+261
View File
@@ -0,0 +1,261 @@
// Copyright 2014 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.
package storage
import (
"context"
"encoding/base64"
"errors"
"fmt"
"io"
"sync"
"unicode/utf8"
"google.golang.org/api/googleapi"
raw "google.golang.org/api/storage/v1"
)
// A Writer writes a Cloud Storage object.
type Writer struct {
// ObjectAttrs are optional attributes to set on the object. Any attributes
// must be initialized before the first Write call. Nil or zero-valued
// attributes are ignored.
ObjectAttrs
// SendCRC specifies whether to transmit a CRC32C field. It should be set
// to true in addition to setting the Writer's CRC32C field, because zero
// is a valid CRC and normally a zero would not be transmitted.
// If a CRC32C is sent, and the data written does not match the checksum,
// the write will be rejected.
SendCRC32C bool
// ChunkSize controls the maximum number of bytes of the object that the
// Writer will attempt to send to the server in a single request. Objects
// smaller than the size will be sent in a single request, while larger
// objects will be split over multiple requests. The size will be rounded up
// to the nearest multiple of 256K. If zero, chunking will be disabled and
// the object will be uploaded in a single request.
//
// ChunkSize will default to a reasonable value. If you perform many concurrent
// writes of small objects, you may wish set ChunkSize to a value that matches
// your objects' sizes to avoid consuming large amounts of memory.
//
// ChunkSize must be set before the first Write call.
ChunkSize int
// 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),
// then ProgressFunc will be invoked after each call with the number of bytes of
// content copied so far.
//
// ProgressFunc should return quickly without blocking.
ProgressFunc func(int64)
ctx context.Context
o *ObjectHandle
opened bool
pw *io.PipeWriter
donec chan struct{} // closed after err and obj are set.
obj *ObjectAttrs
mu sync.Mutex
err error
}
func (w *Writer) open() error {
attrs := w.ObjectAttrs
// Check the developer didn't change the object Name (this is unfortunate, but
// we don't want to store an object under the wrong name).
if attrs.Name != w.o.object {
return fmt.Errorf("storage: Writer.Name %q does not match object name %q", attrs.Name, w.o.object)
}
if !utf8.ValidString(attrs.Name) {
return fmt.Errorf("storage: object name %q is not valid UTF-8", attrs.Name)
}
if attrs.KMSKeyName != "" && w.o.encryptionKey != nil {
return errors.New("storage: cannot use KMSKeyName with a customer-supplied encryption key")
}
pr, pw := io.Pipe()
w.pw = pw
w.opened = true
go w.monitorCancel()
if w.ChunkSize < 0 {
return errors.New("storage: Writer.ChunkSize must be non-negative")
}
mediaOpts := []googleapi.MediaOption{
googleapi.ChunkSize(w.ChunkSize),
}
if c := attrs.ContentType; c != "" {
mediaOpts = append(mediaOpts, googleapi.ContentType(c))
}
go func() {
defer close(w.donec)
rawObj := attrs.toRawObject(w.o.bucket)
if w.SendCRC32C {
rawObj.Crc32c = encodeUint32(attrs.CRC32C)
}
if w.MD5 != nil {
rawObj.Md5Hash = base64.StdEncoding.EncodeToString(w.MD5)
}
call := w.o.c.raw.Objects.Insert(w.o.bucket, rawObj).
Media(pr, mediaOpts...).
Projection("full").
Context(w.ctx)
if w.ProgressFunc != nil {
call.ProgressUpdater(func(n, _ int64) { w.ProgressFunc(n) })
}
if attrs.KMSKeyName != "" {
call.KmsKeyName(attrs.KMSKeyName)
}
if attrs.PredefinedACL != "" {
call.PredefinedAcl(attrs.PredefinedACL)
}
if err := setEncryptionHeaders(call.Header(), w.o.encryptionKey, false); err != nil {
w.mu.Lock()
w.err = err
w.mu.Unlock()
pr.CloseWithError(err)
return
}
var resp *raw.Object
err := applyConds("NewWriter", w.o.gen, w.o.conds, call)
if err == nil {
if w.o.userProject != "" {
call.UserProject(w.o.userProject)
}
setClientHeader(call.Header())
// If the chunk size is zero, then no chunking is done on the Reader,
// which means we cannot retry: the first call will read the data, and if
// it fails, there is no way to re-read.
if w.ChunkSize == 0 {
resp, err = call.Do()
} else {
// We will only retry here if the initial POST, which obtains a URI for
// the resumable upload, fails with a retryable error. The upload itself
// has its own retry logic.
err = runWithRetry(w.ctx, func() error {
var err2 error
resp, err2 = call.Do()
return err2
})
}
}
if err != nil {
w.mu.Lock()
w.err = err
w.mu.Unlock()
pr.CloseWithError(err)
return
}
w.obj = newObject(resp)
}()
return nil
}
// Write appends to w. It implements the io.Writer interface.
//
// Since writes happen asynchronously, Write may return a nil
// error even though the write failed (or will fail). Always
// use the error returned from Writer.Close to determine if
// the upload was successful.
func (w *Writer) Write(p []byte) (n int, err error) {
w.mu.Lock()
werr := w.err
w.mu.Unlock()
if werr != nil {
return 0, werr
}
if !w.opened {
if err := w.open(); err != nil {
return 0, err
}
}
n, err = w.pw.Write(p)
if err != nil {
w.mu.Lock()
werr := w.err
w.mu.Unlock()
// Preserve existing functionality that when context is canceled, Write will return
// context.Canceled instead of "io: read/write on closed pipe". This hides the
// pipe implementation detail from users and makes Write seem as though it's an RPC.
if werr == context.Canceled || werr == context.DeadlineExceeded {
return n, werr
}
}
return n, err
}
// Close completes the write operation and flushes any buffered data.
// If Close doesn't return an error, metadata about the written object
// can be retrieved by calling Attrs.
func (w *Writer) Close() error {
if !w.opened {
if err := w.open(); err != nil {
return err
}
}
// Closing either the read or write causes the entire pipe to close.
if err := w.pw.Close(); err != nil {
return err
}
<-w.donec
w.mu.Lock()
defer w.mu.Unlock()
return w.err
}
// monitorCancel is intended to be used as a background goroutine. It monitors the
// the context, and when it observes that the context has been canceled, it manually
// closes things that do not take a context.
func (w *Writer) monitorCancel() {
select {
case <-w.ctx.Done():
w.mu.Lock()
werr := w.ctx.Err()
w.err = werr
w.mu.Unlock()
// Closing either the read or write causes the entire pipe to close.
w.CloseWithError(werr)
case <-w.donec:
}
}
// CloseWithError aborts the write operation with the provided error.
// CloseWithError always returns nil.
//
// Deprecated: cancel the context passed to NewWriter instead.
func (w *Writer) CloseWithError(err error) error {
if !w.opened {
return nil
}
return w.pw.CloseWithError(err)
}
// Attrs returns metadata about a successfully-written object.
// It's only valid to call it after Close returns nil.
func (w *Writer) Attrs() *ObjectAttrs {
return w.obj
}
-409
View File
@@ -1,409 +0,0 @@
# 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.
-21
View File
@@ -1,21 +0,0 @@
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
-307
View File
@@ -1,307 +0,0 @@
# 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)
-243
View File
@@ -1,243 +0,0 @@
# 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)
@@ -1,205 +0,0 @@
# 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).
-6
View File
@@ -1,6 +0,0 @@
{
"AssetsRepo": "Azure/azure-sdk-assets",
"AssetsRepoPrefixPath": "go",
"TagPrefix": "go/azidentity",
"Tag": "go/azidentity_6225ab0470"
}
-190
View File
@@ -1,190 +0,0 @@
//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)
}
@@ -1,180 +0,0 @@
//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)
@@ -1,138 +0,0 @@
//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)
-47
View File
@@ -1,47 +0,0 @@
# 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
@@ -1,83 +0,0 @@
//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)
@@ -1,172 +0,0 @@
//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)
@@ -1,75 +0,0 @@
//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)
@@ -1,209 +0,0 @@
//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)
}
@@ -1,136 +0,0 @@
//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)
@@ -1,164 +0,0 @@
//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)
-129
View File
@@ -1,129 +0,0 @@
//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)
@@ -1,106 +0,0 @@
//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)
-14
View File
@@ -1,14 +0,0 @@
//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"
@@ -1,388 +0,0 @@
//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
}
@@ -1,127 +0,0 @@
//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)
@@ -1,99 +0,0 @@
//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)
-130
View File
@@ -1,130 +0,0 @@
//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
}
@@ -1,81 +0,0 @@
//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)
-15
View File
@@ -1,15 +0,0 @@
//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"
)
@@ -1,126 +0,0 @@
//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
}
@@ -1,21 +0,0 @@
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
@@ -1,54 +0,0 @@
// 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
}
@@ -1,685 +0,0 @@
// 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)
}
@@ -1,111 +0,0 @@
# 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.
@@ -1,89 +0,0 @@
// 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)
}
@@ -1,467 +0,0 @@
// 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})
}
@@ -1,200 +0,0 @@
// 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,
)
}
@@ -1,436 +0,0 @@
// 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
}
@@ -1,517 +0,0 @@
// 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
}
@@ -1,56 +0,0 @@
{
"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"
}
}
}
@@ -1,34 +0,0 @@
// 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
}
@@ -1,140 +0,0 @@
# 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
@@ -1,184 +0,0 @@
// 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
}
@@ -1,333 +0,0 @@
// 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)
}
@@ -1,346 +0,0 @@
// 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
}
@@ -1,290 +0,0 @@
// 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
}
@@ -1,70 +0,0 @@
// 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
}
@@ -1,177 +0,0 @@
// 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})
}
@@ -1,353 +0,0 @@
// 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
}
@@ -1,451 +0,0 @@
// 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, " "))
}
@@ -1,25 +0,0 @@
// 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]]
}
@@ -1,335 +0,0 @@
// 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)
}
@@ -1,552 +0,0 @@
// 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)
}
@@ -1,30 +0,0 @@
// 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]]
}
@@ -1,320 +0,0 @@
// 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
}
@@ -1,33 +0,0 @@
// 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
}
@@ -1,17 +0,0 @@
// 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"
)
@@ -1,56 +0,0 @@
// 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}
}
@@ -1,25 +0,0 @@
// 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]]
}
@@ -1,394 +0,0 @@
// 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"`
}
@@ -1,230 +0,0 @@
// 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"`
}
@@ -1,25 +0,0 @@
// 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]]
}
@@ -1,199 +0,0 @@
// 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)
}
@@ -1,159 +0,0 @@
// 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
}
@@ -1,136 +0,0 @@
// 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")
}
@@ -1,149 +0,0 @@
// 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
}
@@ -1,52 +0,0 @@
// 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() {}
@@ -1,71 +0,0 @@
// 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{}
@@ -1,8 +0,0 @@
// 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"
@@ -1,683 +0,0 @@
// 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
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,883 @@
// Protocol Buffers - Google's data interchange format
// Copyright 2008 Google Inc. All rights reserved.
// https://developers.google.com/protocol-buffers/
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following disclaimer
// in the documentation and/or other materials provided with the
// distribution.
// * Neither the name of Google Inc. nor the names of its
// contributors may be used to endorse or promote products derived from
// this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
// Author: kenton@google.com (Kenton Varda)
// Based on original Protocol Buffers design by
// Sanjay Ghemawat, Jeff Dean, and others.
//
// The messages in this file describe the definitions found in .proto files.
// A valid .proto file can be translated directly to a FileDescriptorProto
// without any other information (e.g. without reading its imports).
syntax = "proto2";
package google.protobuf;
option go_package = "github.com/golang/protobuf/protoc-gen-go/descriptor;descriptor";
option java_package = "com.google.protobuf";
option java_outer_classname = "DescriptorProtos";
option csharp_namespace = "Google.Protobuf.Reflection";
option objc_class_prefix = "GPB";
option cc_enable_arenas = true;
// descriptor.proto must be optimized for speed because reflection-based
// algorithms don't work during bootstrapping.
option optimize_for = SPEED;
// The protocol compiler can output a FileDescriptorSet containing the .proto
// files it parses.
message FileDescriptorSet {
repeated FileDescriptorProto file = 1;
}
// Describes a complete .proto file.
message FileDescriptorProto {
optional string name = 1; // file name, relative to root of source tree
optional string package = 2; // e.g. "foo", "foo.bar", etc.
// Names of files imported by this file.
repeated string dependency = 3;
// Indexes of the public imported files in the dependency list above.
repeated int32 public_dependency = 10;
// Indexes of the weak imported files in the dependency list.
// For Google-internal migration only. Do not use.
repeated int32 weak_dependency = 11;
// All top-level definitions in this file.
repeated DescriptorProto message_type = 4;
repeated EnumDescriptorProto enum_type = 5;
repeated ServiceDescriptorProto service = 6;
repeated FieldDescriptorProto extension = 7;
optional FileOptions options = 8;
// This field contains optional information about the original source code.
// You may safely remove this entire field without harming runtime
// functionality of the descriptors -- the information is needed only by
// development tools.
optional SourceCodeInfo source_code_info = 9;
// The syntax of the proto file.
// The supported values are "proto2" and "proto3".
optional string syntax = 12;
}
// Describes a message type.
message DescriptorProto {
optional string name = 1;
repeated FieldDescriptorProto field = 2;
repeated FieldDescriptorProto extension = 6;
repeated DescriptorProto nested_type = 3;
repeated EnumDescriptorProto enum_type = 4;
message ExtensionRange {
optional int32 start = 1;
optional int32 end = 2;
optional ExtensionRangeOptions options = 3;
}
repeated ExtensionRange extension_range = 5;
repeated OneofDescriptorProto oneof_decl = 8;
optional MessageOptions options = 7;
// Range of reserved tag numbers. Reserved tag numbers may not be used by
// fields or extension ranges in the same message. Reserved ranges may
// not overlap.
message ReservedRange {
optional int32 start = 1; // Inclusive.
optional int32 end = 2; // Exclusive.
}
repeated ReservedRange reserved_range = 9;
// Reserved field names, which may not be used by fields in the same message.
// A given name may only be reserved once.
repeated string reserved_name = 10;
}
message ExtensionRangeOptions {
// The parser stores options it doesn't recognize here. See above.
repeated UninterpretedOption uninterpreted_option = 999;
// Clients can define custom options in extensions of this message. See above.
extensions 1000 to max;
}
// Describes a field within a message.
message FieldDescriptorProto {
enum Type {
// 0 is reserved for errors.
// Order is weird for historical reasons.
TYPE_DOUBLE = 1;
TYPE_FLOAT = 2;
// Not ZigZag encoded. Negative numbers take 10 bytes. Use TYPE_SINT64 if
// negative values are likely.
TYPE_INT64 = 3;
TYPE_UINT64 = 4;
// Not ZigZag encoded. Negative numbers take 10 bytes. Use TYPE_SINT32 if
// negative values are likely.
TYPE_INT32 = 5;
TYPE_FIXED64 = 6;
TYPE_FIXED32 = 7;
TYPE_BOOL = 8;
TYPE_STRING = 9;
// Tag-delimited aggregate.
// Group type is deprecated and not supported in proto3. However, Proto3
// implementations should still be able to parse the group wire format and
// treat group fields as unknown fields.
TYPE_GROUP = 10;
TYPE_MESSAGE = 11; // Length-delimited aggregate.
// New in version 2.
TYPE_BYTES = 12;
TYPE_UINT32 = 13;
TYPE_ENUM = 14;
TYPE_SFIXED32 = 15;
TYPE_SFIXED64 = 16;
TYPE_SINT32 = 17; // Uses ZigZag encoding.
TYPE_SINT64 = 18; // Uses ZigZag encoding.
};
enum Label {
// 0 is reserved for errors
LABEL_OPTIONAL = 1;
LABEL_REQUIRED = 2;
LABEL_REPEATED = 3;
};
optional string name = 1;
optional int32 number = 3;
optional Label label = 4;
// If type_name is set, this need not be set. If both this and type_name
// are set, this must be one of TYPE_ENUM, TYPE_MESSAGE or TYPE_GROUP.
optional Type type = 5;
// For message and enum types, this is the name of the type. If the name
// starts with a '.', it is fully-qualified. Otherwise, C++-like scoping
// rules are used to find the type (i.e. first the nested types within this
// message are searched, then within the parent, on up to the root
// namespace).
optional string type_name = 6;
// For extensions, this is the name of the type being extended. It is
// resolved in the same manner as type_name.
optional string extendee = 2;
// For numeric types, contains the original text representation of the value.
// For booleans, "true" or "false".
// For strings, contains the default text contents (not escaped in any way).
// For bytes, contains the C escaped value. All bytes >= 128 are escaped.
// TODO(kenton): Base-64 encode?
optional string default_value = 7;
// If set, gives the index of a oneof in the containing type's oneof_decl
// list. This field is a member of that oneof.
optional int32 oneof_index = 9;
// JSON name of this field. The value is set by protocol compiler. If the
// user has set a "json_name" option on this field, that option's value
// will be used. Otherwise, it's deduced from the field's name by converting
// it to camelCase.
optional string json_name = 10;
optional FieldOptions options = 8;
}
// Describes a oneof.
message OneofDescriptorProto {
optional string name = 1;
optional OneofOptions options = 2;
}
// Describes an enum type.
message EnumDescriptorProto {
optional string name = 1;
repeated EnumValueDescriptorProto value = 2;
optional EnumOptions options = 3;
// Range of reserved numeric values. Reserved values may not be used by
// entries in the same enum. Reserved ranges may not overlap.
//
// Note that this is distinct from DescriptorProto.ReservedRange in that it
// is inclusive such that it can appropriately represent the entire int32
// domain.
message EnumReservedRange {
optional int32 start = 1; // Inclusive.
optional int32 end = 2; // Inclusive.
}
// Range of reserved numeric values. Reserved numeric values may not be used
// by enum values in the same enum declaration. Reserved ranges may not
// overlap.
repeated EnumReservedRange reserved_range = 4;
// Reserved enum value names, which may not be reused. A given name may only
// be reserved once.
repeated string reserved_name = 5;
}
// Describes a value within an enum.
message EnumValueDescriptorProto {
optional string name = 1;
optional int32 number = 2;
optional EnumValueOptions options = 3;
}
// Describes a service.
message ServiceDescriptorProto {
optional string name = 1;
repeated MethodDescriptorProto method = 2;
optional ServiceOptions options = 3;
}
// Describes a method of a service.
message MethodDescriptorProto {
optional string name = 1;
// Input and output type names. These are resolved in the same way as
// FieldDescriptorProto.type_name, but must refer to a message type.
optional string input_type = 2;
optional string output_type = 3;
optional MethodOptions options = 4;
// Identifies if client streams multiple client messages
optional bool client_streaming = 5 [default=false];
// Identifies if server streams multiple server messages
optional bool server_streaming = 6 [default=false];
}
// ===================================================================
// Options
// Each of the definitions above may have "options" attached. These are
// just annotations which may cause code to be generated slightly differently
// or may contain hints for code that manipulates protocol messages.
//
// Clients may define custom options as extensions of the *Options messages.
// These extensions may not yet be known at parsing time, so the parser cannot
// store the values in them. Instead it stores them in a field in the *Options
// message called uninterpreted_option. This field must have the same name
// across all *Options messages. We then use this field to populate the
// extensions when we build a descriptor, at which point all protos have been
// parsed and so all extensions are known.
//
// Extension numbers for custom options may be chosen as follows:
// * For options which will only be used within a single application or
// organization, or for experimental options, use field numbers 50000
// through 99999. It is up to you to ensure that you do not use the
// same number for multiple options.
// * For options which will be published and used publicly by multiple
// independent entities, e-mail protobuf-global-extension-registry@google.com
// to reserve extension numbers. Simply provide your project name (e.g.
// Objective-C plugin) and your project website (if available) -- there's no
// need to explain how you intend to use them. Usually you only need one
// extension number. You can declare multiple options with only one extension
// number by putting them in a sub-message. See the Custom Options section of
// the docs for examples:
// https://developers.google.com/protocol-buffers/docs/proto#options
// If this turns out to be popular, a web service will be set up
// to automatically assign option numbers.
message FileOptions {
// Sets the Java package where classes generated from this .proto will be
// placed. By default, the proto package is used, but this is often
// inappropriate because proto packages do not normally start with backwards
// domain names.
optional string java_package = 1;
// If set, all the classes from the .proto file are wrapped in a single
// outer class with the given name. This applies to both Proto1
// (equivalent to the old "--one_java_file" option) and Proto2 (where
// a .proto always translates to a single class, but you may want to
// explicitly choose the class name).
optional string java_outer_classname = 8;
// If set true, then the Java code generator will generate a separate .java
// file for each top-level message, enum, and service defined in the .proto
// file. Thus, these types will *not* be nested inside the outer class
// named by java_outer_classname. However, the outer class will still be
// generated to contain the file's getDescriptor() method as well as any
// top-level extensions defined in the file.
optional bool java_multiple_files = 10 [default=false];
// This option does nothing.
optional bool java_generate_equals_and_hash = 20 [deprecated=true];
// If set true, then the Java2 code generator will generate code that
// throws an exception whenever an attempt is made to assign a non-UTF-8
// byte sequence to a string field.
// Message reflection will do the same.
// However, an extension field still accepts non-UTF-8 byte sequences.
// This option has no effect on when used with the lite runtime.
optional bool java_string_check_utf8 = 27 [default=false];
// Generated classes can be optimized for speed or code size.
enum OptimizeMode {
SPEED = 1; // Generate complete code for parsing, serialization,
// etc.
CODE_SIZE = 2; // Use ReflectionOps to implement these methods.
LITE_RUNTIME = 3; // Generate code using MessageLite and the lite runtime.
}
optional OptimizeMode optimize_for = 9 [default=SPEED];
// Sets the Go package where structs generated from this .proto will be
// placed. If omitted, the Go package will be derived from the following:
// - The basename of the package import path, if provided.
// - Otherwise, the package statement in the .proto file, if present.
// - Otherwise, the basename of the .proto file, without extension.
optional string go_package = 11;
// Should generic services be generated in each language? "Generic" services
// are not specific to any particular RPC system. They are generated by the
// main code generators in each language (without additional plugins).
// Generic services were the only kind of service generation supported by
// early versions of google.protobuf.
//
// Generic services are now considered deprecated in favor of using plugins
// that generate code specific to your particular RPC system. Therefore,
// these default to false. Old code which depends on generic services should
// explicitly set them to true.
optional bool cc_generic_services = 16 [default=false];
optional bool java_generic_services = 17 [default=false];
optional bool py_generic_services = 18 [default=false];
optional bool php_generic_services = 42 [default=false];
// Is this file deprecated?
// Depending on the target platform, this can emit Deprecated annotations
// for everything in the file, or it will be completely ignored; in the very
// least, this is a formalization for deprecating files.
optional bool deprecated = 23 [default=false];
// Enables the use of arenas for the proto messages in this file. This applies
// only to generated classes for C++.
optional bool cc_enable_arenas = 31 [default=false];
// Sets the objective c class prefix which is prepended to all objective c
// generated classes from this .proto. There is no default.
optional string objc_class_prefix = 36;
// Namespace for generated classes; defaults to the package.
optional string csharp_namespace = 37;
// By default Swift generators will take the proto package and CamelCase it
// replacing '.' with underscore and use that to prefix the types/symbols
// defined. When this options is provided, they will use this value instead
// to prefix the types/symbols defined.
optional string swift_prefix = 39;
// Sets the php class prefix which is prepended to all php generated classes
// from this .proto. Default is empty.
optional string php_class_prefix = 40;
// Use this option to change the namespace of php generated classes. Default
// is empty. When this option is empty, the package name will be used for
// determining the namespace.
optional string php_namespace = 41;
// Use this option to change the namespace of php generated metadata classes.
// Default is empty. When this option is empty, the proto file name will be used
// for determining the namespace.
optional string php_metadata_namespace = 44;
// Use this option to change the package of ruby generated classes. Default
// is empty. When this option is not set, the package name will be used for
// determining the ruby package.
optional string ruby_package = 45;
// The parser stores options it doesn't recognize here.
// See the documentation for the "Options" section above.
repeated UninterpretedOption uninterpreted_option = 999;
// Clients can define custom options in extensions of this message.
// See the documentation for the "Options" section above.
extensions 1000 to max;
reserved 38;
}
message MessageOptions {
// Set true to use the old proto1 MessageSet wire format for extensions.
// This is provided for backwards-compatibility with the MessageSet wire
// format. You should not use this for any other reason: It's less
// efficient, has fewer features, and is more complicated.
//
// The message must be defined exactly as follows:
// message Foo {
// option message_set_wire_format = true;
// extensions 4 to max;
// }
// Note that the message cannot have any defined fields; MessageSets only
// have extensions.
//
// All extensions of your type must be singular messages; e.g. they cannot
// be int32s, enums, or repeated messages.
//
// Because this is an option, the above two restrictions are not enforced by
// the protocol compiler.
optional bool message_set_wire_format = 1 [default=false];
// Disables the generation of the standard "descriptor()" accessor, which can
// conflict with a field of the same name. This is meant to make migration
// from proto1 easier; new code should avoid fields named "descriptor".
optional bool no_standard_descriptor_accessor = 2 [default=false];
// Is this message deprecated?
// Depending on the target platform, this can emit Deprecated annotations
// for the message, or it will be completely ignored; in the very least,
// this is a formalization for deprecating messages.
optional bool deprecated = 3 [default=false];
// Whether the message is an automatically generated map entry type for the
// maps field.
//
// For maps fields:
// map<KeyType, ValueType> map_field = 1;
// The parsed descriptor looks like:
// message MapFieldEntry {
// option map_entry = true;
// optional KeyType key = 1;
// optional ValueType value = 2;
// }
// repeated MapFieldEntry map_field = 1;
//
// Implementations may choose not to generate the map_entry=true message, but
// use a native map in the target language to hold the keys and values.
// The reflection APIs in such implementions still need to work as
// if the field is a repeated message field.
//
// NOTE: Do not set the option in .proto files. Always use the maps syntax
// instead. The option should only be implicitly set by the proto compiler
// parser.
optional bool map_entry = 7;
reserved 8; // javalite_serializable
reserved 9; // javanano_as_lite
// The parser stores options it doesn't recognize here. See above.
repeated UninterpretedOption uninterpreted_option = 999;
// Clients can define custom options in extensions of this message. See above.
extensions 1000 to max;
}
message FieldOptions {
// The ctype option instructs the C++ code generator to use a different
// representation of the field than it normally would. See the specific
// options below. This option is not yet implemented in the open source
// release -- sorry, we'll try to include it in a future version!
optional CType ctype = 1 [default = STRING];
enum CType {
// Default mode.
STRING = 0;
CORD = 1;
STRING_PIECE = 2;
}
// The packed option can be enabled for repeated primitive fields to enable
// a more efficient representation on the wire. Rather than repeatedly
// writing the tag and type for each element, the entire array is encoded as
// a single length-delimited blob. In proto3, only explicit setting it to
// false will avoid using packed encoding.
optional bool packed = 2;
// The jstype option determines the JavaScript type used for values of the
// field. The option is permitted only for 64 bit integral and fixed types
// (int64, uint64, sint64, fixed64, sfixed64). A field with jstype JS_STRING
// is represented as JavaScript string, which avoids loss of precision that
// can happen when a large value is converted to a floating point JavaScript.
// Specifying JS_NUMBER for the jstype causes the generated JavaScript code to
// use the JavaScript "number" type. The behavior of the default option
// JS_NORMAL is implementation dependent.
//
// This option is an enum to permit additional types to be added, e.g.
// goog.math.Integer.
optional JSType jstype = 6 [default = JS_NORMAL];
enum JSType {
// Use the default type.
JS_NORMAL = 0;
// Use JavaScript strings.
JS_STRING = 1;
// Use JavaScript numbers.
JS_NUMBER = 2;
}
// Should this field be parsed lazily? Lazy applies only to message-type
// fields. It means that when the outer message is initially parsed, the
// inner message's contents will not be parsed but instead stored in encoded
// form. The inner message will actually be parsed when it is first accessed.
//
// This is only a hint. Implementations are free to choose whether to use
// eager or lazy parsing regardless of the value of this option. However,
// setting this option true suggests that the protocol author believes that
// using lazy parsing on this field is worth the additional bookkeeping
// overhead typically needed to implement it.
//
// This option does not affect the public interface of any generated code;
// all method signatures remain the same. Furthermore, thread-safety of the
// interface is not affected by this option; const methods remain safe to
// call from multiple threads concurrently, while non-const methods continue
// to require exclusive access.
//
//
// Note that implementations may choose not to check required fields within
// a lazy sub-message. That is, calling IsInitialized() on the outer message
// may return true even if the inner message has missing required fields.
// This is necessary because otherwise the inner message would have to be
// parsed in order to perform the check, defeating the purpose of lazy
// parsing. An implementation which chooses not to check required fields
// must be consistent about it. That is, for any particular sub-message, the
// implementation must either *always* check its required fields, or *never*
// check its required fields, regardless of whether or not the message has
// been parsed.
optional bool lazy = 5 [default=false];
// Is this field deprecated?
// Depending on the target platform, this can emit Deprecated annotations
// for accessors, or it will be completely ignored; in the very least, this
// is a formalization for deprecating fields.
optional bool deprecated = 3 [default=false];
// For Google-internal migration only. Do not use.
optional bool weak = 10 [default=false];
// The parser stores options it doesn't recognize here. See above.
repeated UninterpretedOption uninterpreted_option = 999;
// Clients can define custom options in extensions of this message. See above.
extensions 1000 to max;
reserved 4; // removed jtype
}
message OneofOptions {
// The parser stores options it doesn't recognize here. See above.
repeated UninterpretedOption uninterpreted_option = 999;
// Clients can define custom options in extensions of this message. See above.
extensions 1000 to max;
}
message EnumOptions {
// Set this option to true to allow mapping different tag names to the same
// value.
optional bool allow_alias = 2;
// Is this enum deprecated?
// Depending on the target platform, this can emit Deprecated annotations
// for the enum, or it will be completely ignored; in the very least, this
// is a formalization for deprecating enums.
optional bool deprecated = 3 [default=false];
reserved 5; // javanano_as_lite
// The parser stores options it doesn't recognize here. See above.
repeated UninterpretedOption uninterpreted_option = 999;
// Clients can define custom options in extensions of this message. See above.
extensions 1000 to max;
}
message EnumValueOptions {
// Is this enum value deprecated?
// Depending on the target platform, this can emit Deprecated annotations
// for the enum value, or it will be completely ignored; in the very least,
// this is a formalization for deprecating enum values.
optional bool deprecated = 1 [default=false];
// The parser stores options it doesn't recognize here. See above.
repeated UninterpretedOption uninterpreted_option = 999;
// Clients can define custom options in extensions of this message. See above.
extensions 1000 to max;
}
message ServiceOptions {
// Note: Field numbers 1 through 32 are reserved for Google's internal RPC
// framework. We apologize for hoarding these numbers to ourselves, but
// we were already using them long before we decided to release Protocol
// Buffers.
// Is this service deprecated?
// Depending on the target platform, this can emit Deprecated annotations
// for the service, or it will be completely ignored; in the very least,
// this is a formalization for deprecating services.
optional bool deprecated = 33 [default=false];
// The parser stores options it doesn't recognize here. See above.
repeated UninterpretedOption uninterpreted_option = 999;
// Clients can define custom options in extensions of this message. See above.
extensions 1000 to max;
}
message MethodOptions {
// Note: Field numbers 1 through 32 are reserved for Google's internal RPC
// framework. We apologize for hoarding these numbers to ourselves, but
// we were already using them long before we decided to release Protocol
// Buffers.
// Is this method deprecated?
// Depending on the target platform, this can emit Deprecated annotations
// for the method, or it will be completely ignored; in the very least,
// this is a formalization for deprecating methods.
optional bool deprecated = 33 [default=false];
// Is this method side-effect-free (or safe in HTTP parlance), or idempotent,
// or neither? HTTP based RPC implementation may choose GET verb for safe
// methods, and PUT verb for idempotent methods instead of the default POST.
enum IdempotencyLevel {
IDEMPOTENCY_UNKNOWN = 0;
NO_SIDE_EFFECTS = 1; // implies idempotent
IDEMPOTENT = 2; // idempotent, but may have side effects
}
optional IdempotencyLevel idempotency_level =
34 [default=IDEMPOTENCY_UNKNOWN];
// The parser stores options it doesn't recognize here. See above.
repeated UninterpretedOption uninterpreted_option = 999;
// Clients can define custom options in extensions of this message. See above.
extensions 1000 to max;
}
// A message representing a option the parser does not recognize. This only
// appears in options protos created by the compiler::Parser class.
// DescriptorPool resolves these when building Descriptor objects. Therefore,
// options protos in descriptor objects (e.g. returned by Descriptor::options(),
// or produced by Descriptor::CopyTo()) will never have UninterpretedOptions
// in them.
message UninterpretedOption {
// The name of the uninterpreted option. Each string represents a segment in
// a dot-separated name. is_extension is true iff a segment represents an
// extension (denoted with parentheses in options specs in .proto files).
// E.g.,{ ["foo", false], ["bar.baz", true], ["qux", false] } represents
// "foo.(bar.baz).qux".
message NamePart {
required string name_part = 1;
required bool is_extension = 2;
}
repeated NamePart name = 2;
// The value of the uninterpreted option, in whatever type the tokenizer
// identified it as during parsing. Exactly one of these should be set.
optional string identifier_value = 3;
optional uint64 positive_int_value = 4;
optional int64 negative_int_value = 5;
optional double double_value = 6;
optional bytes string_value = 7;
optional string aggregate_value = 8;
}
// ===================================================================
// Optional source code info
// Encapsulates information about the original source file from which a
// FileDescriptorProto was generated.
message SourceCodeInfo {
// A Location identifies a piece of source code in a .proto file which
// corresponds to a particular definition. This information is intended
// to be useful to IDEs, code indexers, documentation generators, and similar
// tools.
//
// For example, say we have a file like:
// message Foo {
// optional string foo = 1;
// }
// Let's look at just the field definition:
// optional string foo = 1;
// ^ ^^ ^^ ^ ^^^
// a bc de f ghi
// We have the following locations:
// span path represents
// [a,i) [ 4, 0, 2, 0 ] The whole field definition.
// [a,b) [ 4, 0, 2, 0, 4 ] The label (optional).
// [c,d) [ 4, 0, 2, 0, 5 ] The type (string).
// [e,f) [ 4, 0, 2, 0, 1 ] The name (foo).
// [g,h) [ 4, 0, 2, 0, 3 ] The number (1).
//
// Notes:
// - A location may refer to a repeated field itself (i.e. not to any
// particular index within it). This is used whenever a set of elements are
// logically enclosed in a single code segment. For example, an entire
// extend block (possibly containing multiple extension definitions) will
// have an outer location whose path refers to the "extensions" repeated
// field without an index.
// - Multiple locations may have the same path. This happens when a single
// logical declaration is spread out across multiple places. The most
// obvious example is the "extend" block again -- there may be multiple
// extend blocks in the same scope, each of which will have the same path.
// - A location's span is not always a subset of its parent's span. For
// example, the "extendee" of an extension declaration appears at the
// beginning of the "extend" block and is shared by all extensions within
// the block.
// - Just because a location's span is a subset of some other location's span
// does not mean that it is a descendent. For example, a "group" defines
// both a type and a field in a single declaration. Thus, the locations
// corresponding to the type and field and their components will overlap.
// - Code which tries to interpret locations should probably be designed to
// ignore those that it doesn't understand, as more types of locations could
// be recorded in the future.
repeated Location location = 1;
message Location {
// Identifies which part of the FileDescriptorProto was defined at this
// location.
//
// Each element is a field number or an index. They form a path from
// the root FileDescriptorProto to the place where the definition. For
// example, this path:
// [ 4, 3, 2, 7, 1 ]
// refers to:
// file.message_type(3) // 4, 3
// .field(7) // 2, 7
// .name() // 1
// This is because FileDescriptorProto.message_type has field number 4:
// repeated DescriptorProto message_type = 4;
// and DescriptorProto.field has field number 2:
// repeated FieldDescriptorProto field = 2;
// and FieldDescriptorProto.name has field number 1:
// optional string name = 1;
//
// Thus, the above path gives the location of a field name. If we removed
// the last element:
// [ 4, 3, 2, 7 ]
// this path refers to the whole field declaration (from the beginning
// of the label to the terminating semicolon).
repeated int32 path = 1 [packed=true];
// Always has exactly three or four elements: start line, start column,
// end line (optional, otherwise assumed same as start line), end column.
// These are packed into a single field for efficiency. Note that line
// and column numbers are zero-based -- typically you will want to add
// 1 to each before displaying to a user.
repeated int32 span = 2 [packed=true];
// If this SourceCodeInfo represents a complete declaration, these are any
// comments appearing before and after the declaration which appear to be
// attached to the declaration.
//
// A series of line comments appearing on consecutive lines, with no other
// tokens appearing on those lines, will be treated as a single comment.
//
// leading_detached_comments will keep paragraphs of comments that appear
// before (but not connected to) the current element. Each paragraph,
// separated by empty lines, will be one comment element in the repeated
// field.
//
// Only the comment content is provided; comment markers (e.g. //) are
// stripped out. For block comments, leading whitespace and an asterisk
// will be stripped from the beginning of each line other than the first.
// Newlines are included in the output.
//
// Examples:
//
// optional int32 foo = 1; // Comment attached to foo.
// // Comment attached to bar.
// optional int32 bar = 2;
//
// optional string baz = 3;
// // Comment attached to baz.
// // Another line attached to baz.
//
// // Comment attached to qux.
// //
// // Another line attached to qux.
// optional double qux = 4;
//
// // Detached comment for corge. This is not leading or trailing comments
// // to qux or corge because there are blank lines separating it from
// // both.
//
// // Detached comment for corge paragraph 2.
//
// optional string corge = 5;
// /* Block comment attached
// * to corge. Leading asterisks
// * will be removed. */
// /* Block comment attached to
// * grault. */
// optional int32 grault = 6;
//
// // ignored detached comments.
optional string leading_comments = 3;
optional string trailing_comments = 4;
repeated string leading_detached_comments = 6;
}
}
// Describes the relationship between generated code and its original source
// file. A GeneratedCodeInfo message is associated with only one generated
// source file, but may contain references to different source .proto files.
message GeneratedCodeInfo {
// An Annotation connects some span of text in generated code to an element
// of its generating .proto file.
repeated Annotation annotation = 1;
message Annotation {
// Identifies the element in the original source .proto file. This field
// is formatted the same as SourceCodeInfo.Location.path.
repeated int32 path = 1 [packed=true];
// Identifies the filesystem path to the original source .proto.
optional string source_file = 2;
// Identifies the starting offset in bytes in the generated code
// that relates to the identified object.
optional int32 begin = 3;
// Identifies the ending offset in bytes in the generated code that
// relates to the identified offset. The end offset should be one past
// the last relevant byte (so the length of the text = end - begin).
optional int32 end = 4;
}
}
-202
View File
@@ -1,202 +0,0 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
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.
-186
View File
@@ -1,186 +0,0 @@
// Copyright 2013 Google Inc. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package diff implements a linewise diff algorithm.
package diff
import (
"bytes"
"fmt"
"strings"
)
// Chunk represents a piece of the diff. A chunk will not have both added and
// deleted lines. Equal lines are always after any added or deleted lines.
// A Chunk may or may not have any lines in it, especially for the first or last
// chunk in a computation.
type Chunk struct {
Added []string
Deleted []string
Equal []string
}
func (c *Chunk) empty() bool {
return len(c.Added) == 0 && len(c.Deleted) == 0 && len(c.Equal) == 0
}
// Diff returns a string containing a line-by-line unified diff of the linewise
// changes required to make A into B. Each line is prefixed with '+', '-', or
// ' ' to indicate if it should be added, removed, or is correct respectively.
func Diff(A, B string) string {
aLines := strings.Split(A, "\n")
bLines := strings.Split(B, "\n")
chunks := DiffChunks(aLines, bLines)
buf := new(bytes.Buffer)
for _, c := range chunks {
for _, line := range c.Added {
fmt.Fprintf(buf, "+%s\n", line)
}
for _, line := range c.Deleted {
fmt.Fprintf(buf, "-%s\n", line)
}
for _, line := range c.Equal {
fmt.Fprintf(buf, " %s\n", line)
}
}
return strings.TrimRight(buf.String(), "\n")
}
// DiffChunks uses an O(D(N+M)) shortest-edit-script algorithm
// to compute the edits required from A to B and returns the
// edit chunks.
func DiffChunks(a, b []string) []Chunk {
// algorithm: http://www.xmailserver.org/diff2.pdf
// We'll need these quantities a lot.
alen, blen := len(a), len(b) // M, N
// At most, it will require len(a) deletions and len(b) additions
// to transform a into b.
maxPath := alen + blen // MAX
if maxPath == 0 {
// degenerate case: two empty lists are the same
return nil
}
// Store the endpoint of the path for diagonals.
// We store only the a index, because the b index on any diagonal
// (which we know during the loop below) is aidx-diag.
// endpoint[maxPath] represents the 0 diagonal.
//
// Stated differently:
// endpoint[d] contains the aidx of a furthest reaching path in diagonal d
endpoint := make([]int, 2*maxPath+1) // V
saved := make([][]int, 0, 8) // Vs
save := func() {
dup := make([]int, len(endpoint))
copy(dup, endpoint)
saved = append(saved, dup)
}
var editDistance int // D
dLoop:
for editDistance = 0; editDistance <= maxPath; editDistance++ {
// The 0 diag(onal) represents equality of a and b. Each diagonal to
// the left is numbered one lower, to the right is one higher, from
// -alen to +blen. Negative diagonals favor differences from a,
// positive diagonals favor differences from b. The edit distance to a
// diagonal d cannot be shorter than d itself.
//
// The iterations of this loop cover either odds or evens, but not both,
// If odd indices are inputs, even indices are outputs and vice versa.
for diag := -editDistance; diag <= editDistance; diag += 2 { // k
var aidx int // x
switch {
case diag == -editDistance:
// This is a new diagonal; copy from previous iter
aidx = endpoint[maxPath-editDistance+1] + 0
case diag == editDistance:
// This is a new diagonal; copy from previous iter
aidx = endpoint[maxPath+editDistance-1] + 1
case endpoint[maxPath+diag+1] > endpoint[maxPath+diag-1]:
// diagonal d+1 was farther along, so use that
aidx = endpoint[maxPath+diag+1] + 0
default:
// diagonal d-1 was farther (or the same), so use that
aidx = endpoint[maxPath+diag-1] + 1
}
// On diagonal d, we can compute bidx from aidx.
bidx := aidx - diag // y
// See how far we can go on this diagonal before we find a difference.
for aidx < alen && bidx < blen && a[aidx] == b[bidx] {
aidx++
bidx++
}
// Store the end of the current edit chain.
endpoint[maxPath+diag] = aidx
// If we've found the end of both inputs, we're done!
if aidx >= alen && bidx >= blen {
save() // save the final path
break dLoop
}
}
save() // save the current path
}
if editDistance == 0 {
return nil
}
chunks := make([]Chunk, editDistance+1)
x, y := alen, blen
for d := editDistance; d > 0; d-- {
endpoint := saved[d]
diag := x - y
insert := diag == -d || (diag != d && endpoint[maxPath+diag-1] < endpoint[maxPath+diag+1])
x1 := endpoint[maxPath+diag]
var x0, xM, kk int
if insert {
kk = diag + 1
x0 = endpoint[maxPath+kk]
xM = x0
} else {
kk = diag - 1
x0 = endpoint[maxPath+kk]
xM = x0 + 1
}
y0 := x0 - kk
var c Chunk
if insert {
c.Added = b[y0:][:1]
} else {
c.Deleted = a[x0:][:1]
}
if xM < x1 {
c.Equal = a[xM:][:x1-xM]
}
x, y = x0, y0
chunks[d] = c
}
if x > 0 {
chunks[0].Equal = a[:x]
}
if chunks[0].empty() {
chunks = chunks[1:]
}
if len(chunks) == 0 {
return nil
}
return chunks
}
-5
View File
@@ -1,5 +0,0 @@
*.test
*.bench
*.golden
*.txt
*.prof
-188
View File
@@ -1,188 +0,0 @@
// Copyright 2013 Google Inc. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package pretty
import (
"bytes"
"fmt"
"io"
"net"
"reflect"
"time"
"github.com/kylelemons/godebug/diff"
)
// A Config represents optional configuration parameters for formatting.
//
// Some options, notably ShortList, dramatically increase the overhead
// of pretty-printing a value.
type Config struct {
// Verbosity options
Compact bool // One-line output. Overrides Diffable.
Diffable bool // Adds extra newlines for more easily diffable output.
// Field and value options
IncludeUnexported bool // Include unexported fields in output
PrintStringers bool // Call String on a fmt.Stringer
PrintTextMarshalers bool // Call MarshalText on an encoding.TextMarshaler
SkipZeroFields bool // Skip struct fields that have a zero value.
// Output transforms
ShortList int // Maximum character length for short lists if nonzero.
// Type-specific overrides
//
// Formatter maps a type to a function that will provide a one-line string
// representation of the input value. Conceptually:
// Formatter[reflect.TypeOf(v)](v) = "v as a string"
//
// Note that the first argument need not explicitly match the type, it must
// merely be callable with it.
//
// When processing an input value, if its type exists as a key in Formatter:
// 1) If the value is nil, no stringification is performed.
// This allows overriding of PrintStringers and PrintTextMarshalers.
// 2) The value will be called with the input as its only argument.
// The function must return a string as its first return value.
//
// In addition to func literals, two common values for this will be:
// fmt.Sprint (function) func Sprint(...interface{}) string
// Type.String (method) func (Type) String() string
//
// Note that neither of these work if the String method is a pointer
// method and the input will be provided as a value. In that case,
// use a function that calls .String on the formal value parameter.
Formatter map[reflect.Type]interface{}
// If TrackCycles is enabled, pretty will detect and track
// self-referential structures. If a self-referential structure (aka a
// "recursive" value) is detected, numbered placeholders will be emitted.
//
// Pointer tracking is disabled by default for performance reasons.
TrackCycles bool
}
// Default Config objects
var (
// DefaultFormatter is the default set of overrides for stringification.
DefaultFormatter = map[reflect.Type]interface{}{
reflect.TypeOf(time.Time{}): fmt.Sprint,
reflect.TypeOf(net.IP{}): fmt.Sprint,
reflect.TypeOf((*error)(nil)).Elem(): fmt.Sprint,
}
// CompareConfig is the default configuration used for Compare.
CompareConfig = &Config{
Diffable: true,
IncludeUnexported: true,
Formatter: DefaultFormatter,
}
// DefaultConfig is the default configuration used for all other top-level functions.
DefaultConfig = &Config{
Formatter: DefaultFormatter,
}
// CycleTracker is a convenience config for formatting and comparing recursive structures.
CycleTracker = &Config{
Diffable: true,
Formatter: DefaultFormatter,
TrackCycles: true,
}
)
func (cfg *Config) fprint(buf *bytes.Buffer, vals ...interface{}) {
ref := &reflector{
Config: cfg,
}
if cfg.TrackCycles {
ref.pointerTracker = new(pointerTracker)
}
for i, val := range vals {
if i > 0 {
buf.WriteByte('\n')
}
newFormatter(cfg, buf).write(ref.val2node(reflect.ValueOf(val)))
}
}
// Print writes the DefaultConfig representation of the given values to standard output.
func Print(vals ...interface{}) {
DefaultConfig.Print(vals...)
}
// Print writes the configured presentation of the given values to standard output.
func (cfg *Config) Print(vals ...interface{}) {
fmt.Println(cfg.Sprint(vals...))
}
// Sprint returns a string representation of the given value according to the DefaultConfig.
func Sprint(vals ...interface{}) string {
return DefaultConfig.Sprint(vals...)
}
// Sprint returns a string representation of the given value according to cfg.
func (cfg *Config) Sprint(vals ...interface{}) string {
buf := new(bytes.Buffer)
cfg.fprint(buf, vals...)
return buf.String()
}
// Fprint writes the representation of the given value to the writer according to the DefaultConfig.
func Fprint(w io.Writer, vals ...interface{}) (n int64, err error) {
return DefaultConfig.Fprint(w, vals...)
}
// Fprint writes the representation of the given value to the writer according to the cfg.
func (cfg *Config) Fprint(w io.Writer, vals ...interface{}) (n int64, err error) {
buf := new(bytes.Buffer)
cfg.fprint(buf, vals...)
return buf.WriteTo(w)
}
// Compare returns a string containing a line-by-line unified diff of the
// values in a and b, using the CompareConfig.
//
// Each line in the output is prefixed with '+', '-', or ' ' to indicate which
// side it's from. Lines from the a side are marked with '-', lines from the
// b side are marked with '+' and lines that are the same on both sides are
// marked with ' '.
//
// The comparison is based on the intentionally-untyped output of Print, and as
// such this comparison is pretty forviving. In particular, if the types of or
// types within in a and b are different but have the same representation,
// Compare will not indicate any differences between them.
func Compare(a, b interface{}) string {
return CompareConfig.Compare(a, b)
}
// Compare returns a string containing a line-by-line unified diff of the
// values in got and want according to the cfg.
//
// Each line in the output is prefixed with '+', '-', or ' ' to indicate which
// side it's from. Lines from the a side are marked with '-', lines from the
// b side are marked with '+' and lines that are the same on both sides are
// marked with ' '.
//
// The comparison is based on the intentionally-untyped output of Print, and as
// such this comparison is pretty forviving. In particular, if the types of or
// types within in a and b are different but have the same representation,
// Compare will not indicate any differences between them.
func (cfg *Config) Compare(a, b interface{}) string {
diffCfg := *cfg
diffCfg.Diffable = true
return diff.Diff(cfg.Sprint(a), cfg.Sprint(b))
}
-241
View File
@@ -1,241 +0,0 @@
// Copyright 2013 Google Inc. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package pretty
import (
"encoding"
"fmt"
"reflect"
"sort"
)
func isZeroVal(val reflect.Value) bool {
if !val.CanInterface() {
return false
}
z := reflect.Zero(val.Type()).Interface()
return reflect.DeepEqual(val.Interface(), z)
}
// pointerTracker is a helper for tracking pointer chasing to detect cycles.
type pointerTracker struct {
addrs map[uintptr]int // addr[address] = seen count
lastID int
ids map[uintptr]int // ids[address] = id
}
// track tracks following a reference (pointer, slice, map, etc). Every call to
// track should be paired with a call to untrack.
func (p *pointerTracker) track(ptr uintptr) {
if p.addrs == nil {
p.addrs = make(map[uintptr]int)
}
p.addrs[ptr]++
}
// untrack registers that we have backtracked over the reference to the pointer.
func (p *pointerTracker) untrack(ptr uintptr) {
p.addrs[ptr]--
if p.addrs[ptr] == 0 {
delete(p.addrs, ptr)
}
}
// seen returns whether the pointer was previously seen along this path.
func (p *pointerTracker) seen(ptr uintptr) bool {
_, ok := p.addrs[ptr]
return ok
}
// keep allocates an ID for the given address and returns it.
func (p *pointerTracker) keep(ptr uintptr) int {
if p.ids == nil {
p.ids = make(map[uintptr]int)
}
if _, ok := p.ids[ptr]; !ok {
p.lastID++
p.ids[ptr] = p.lastID
}
return p.ids[ptr]
}
// id returns the ID for the given address.
func (p *pointerTracker) id(ptr uintptr) (int, bool) {
if p.ids == nil {
p.ids = make(map[uintptr]int)
}
id, ok := p.ids[ptr]
return id, ok
}
// reflector adds local state to the recursive reflection logic.
type reflector struct {
*Config
*pointerTracker
}
// follow handles following a possiblly-recursive reference to the given value
// from the given ptr address.
func (r *reflector) follow(ptr uintptr, val reflect.Value) node {
if r.pointerTracker == nil {
// Tracking disabled
return r.val2node(val)
}
// If a parent already followed this, emit a reference marker
if r.seen(ptr) {
id := r.keep(ptr)
return ref{id}
}
// Track the pointer we're following while on this recursive branch
r.track(ptr)
defer r.untrack(ptr)
n := r.val2node(val)
// If the recursion used this ptr, wrap it with a target marker
if id, ok := r.id(ptr); ok {
return target{id, n}
}
// Otherwise, return the node unadulterated
return n
}
func (r *reflector) val2node(val reflect.Value) node {
if !val.IsValid() {
return rawVal("nil")
}
if val.CanInterface() {
v := val.Interface()
if formatter, ok := r.Formatter[val.Type()]; ok {
if formatter != nil {
res := reflect.ValueOf(formatter).Call([]reflect.Value{val})
return rawVal(res[0].Interface().(string))
}
} else {
if s, ok := v.(fmt.Stringer); ok && r.PrintStringers {
return stringVal(s.String())
}
if t, ok := v.(encoding.TextMarshaler); ok && r.PrintTextMarshalers {
if raw, err := t.MarshalText(); err == nil { // if NOT an error
return stringVal(string(raw))
}
}
}
}
switch kind := val.Kind(); kind {
case reflect.Ptr:
if val.IsNil() {
return rawVal("nil")
}
return r.follow(val.Pointer(), val.Elem())
case reflect.Interface:
if val.IsNil() {
return rawVal("nil")
}
return r.val2node(val.Elem())
case reflect.String:
return stringVal(val.String())
case reflect.Slice:
n := list{}
length := val.Len()
ptr := val.Pointer()
for i := 0; i < length; i++ {
n = append(n, r.follow(ptr, val.Index(i)))
}
return n
case reflect.Array:
n := list{}
length := val.Len()
for i := 0; i < length; i++ {
n = append(n, r.val2node(val.Index(i)))
}
return n
case reflect.Map:
// Extract the keys and sort them for stable iteration
keys := val.MapKeys()
pairs := make([]mapPair, 0, len(keys))
for _, key := range keys {
pairs = append(pairs, mapPair{
key: new(formatter).compactString(r.val2node(key)), // can't be cyclic
value: val.MapIndex(key),
})
}
sort.Sort(byKey(pairs))
// Process the keys into the final representation
ptr, n := val.Pointer(), keyvals{}
for _, pair := range pairs {
n = append(n, keyval{
key: pair.key,
val: r.follow(ptr, pair.value),
})
}
return n
case reflect.Struct:
n := keyvals{}
typ := val.Type()
fields := typ.NumField()
for i := 0; i < fields; i++ {
sf := typ.Field(i)
if !r.IncludeUnexported && sf.PkgPath != "" {
continue
}
field := val.Field(i)
if r.SkipZeroFields && isZeroVal(field) {
continue
}
n = append(n, keyval{sf.Name, r.val2node(field)})
}
return n
case reflect.Bool:
if val.Bool() {
return rawVal("true")
}
return rawVal("false")
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return rawVal(fmt.Sprintf("%d", val.Int()))
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
return rawVal(fmt.Sprintf("%d", val.Uint()))
case reflect.Uintptr:
return rawVal(fmt.Sprintf("0x%X", val.Uint()))
case reflect.Float32, reflect.Float64:
return rawVal(fmt.Sprintf("%v", val.Float()))
case reflect.Complex64, reflect.Complex128:
return rawVal(fmt.Sprintf("%v", val.Complex()))
}
// Fall back to the default %#v if we can
if val.CanInterface() {
return rawVal(fmt.Sprintf("%#v", val.Interface()))
}
return rawVal(val.String())
}
type mapPair struct {
key string
value reflect.Value
}
type byKey []mapPair
func (v byKey) Len() int { return len(v) }
func (v byKey) Swap(i, j int) { v[i], v[j] = v[j], v[i] }
func (v byKey) Less(i, j int) bool { return v[i].key < v[j].key }
-223
View File
@@ -1,223 +0,0 @@
// Copyright 2013 Google Inc. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package pretty
import (
"bufio"
"bytes"
"fmt"
"io"
"strconv"
"strings"
)
// a formatter stores stateful formatting information as well as being
// an io.Writer for simplicity.
type formatter struct {
*bufio.Writer
*Config
// Self-referential structure tracking
tagNumbers map[int]int // tagNumbers[id] = <#n>
}
// newFormatter creates a new buffered formatter. For the output to be written
// to the given writer, this must be accompanied by a call to write (or Flush).
func newFormatter(cfg *Config, w io.Writer) *formatter {
return &formatter{
Writer: bufio.NewWriter(w),
Config: cfg,
tagNumbers: make(map[int]int),
}
}
func (f *formatter) write(n node) {
defer f.Flush()
n.format(f, "")
}
func (f *formatter) tagFor(id int) int {
if tag, ok := f.tagNumbers[id]; ok {
return tag
}
if f.tagNumbers == nil {
return 0
}
tag := len(f.tagNumbers) + 1
f.tagNumbers[id] = tag
return tag
}
type node interface {
format(f *formatter, indent string)
}
func (f *formatter) compactString(n node) string {
switch k := n.(type) {
case stringVal:
return string(k)
case rawVal:
return string(k)
}
buf := new(bytes.Buffer)
f2 := newFormatter(&Config{Compact: true}, buf)
f2.tagNumbers = f.tagNumbers // reuse tagNumbers just in case
f2.write(n)
return buf.String()
}
type stringVal string
func (str stringVal) format(f *formatter, indent string) {
f.WriteString(strconv.Quote(string(str)))
}
type rawVal string
func (r rawVal) format(f *formatter, indent string) {
f.WriteString(string(r))
}
type keyval struct {
key string
val node
}
type keyvals []keyval
func (l keyvals) format(f *formatter, indent string) {
f.WriteByte('{')
switch {
case f.Compact:
// All on one line:
for i, kv := range l {
if i > 0 {
f.WriteByte(',')
}
f.WriteString(kv.key)
f.WriteByte(':')
kv.val.format(f, indent)
}
case f.Diffable:
f.WriteByte('\n')
inner := indent + " "
// Each value gets its own line:
for _, kv := range l {
f.WriteString(inner)
f.WriteString(kv.key)
f.WriteString(": ")
kv.val.format(f, inner)
f.WriteString(",\n")
}
f.WriteString(indent)
default:
keyWidth := 0
for _, kv := range l {
if kw := len(kv.key); kw > keyWidth {
keyWidth = kw
}
}
alignKey := indent + " "
alignValue := strings.Repeat(" ", keyWidth)
inner := alignKey + alignValue + " "
// First and last line shared with bracket:
for i, kv := range l {
if i > 0 {
f.WriteString(",\n")
f.WriteString(alignKey)
}
f.WriteString(kv.key)
f.WriteString(": ")
f.WriteString(alignValue[len(kv.key):])
kv.val.format(f, inner)
}
}
f.WriteByte('}')
}
type list []node
func (l list) format(f *formatter, indent string) {
if max := f.ShortList; max > 0 {
short := f.compactString(l)
if len(short) <= max {
f.WriteString(short)
return
}
}
f.WriteByte('[')
switch {
case f.Compact:
// All on one line:
for i, v := range l {
if i > 0 {
f.WriteByte(',')
}
v.format(f, indent)
}
case f.Diffable:
f.WriteByte('\n')
inner := indent + " "
// Each value gets its own line:
for _, v := range l {
f.WriteString(inner)
v.format(f, inner)
f.WriteString(",\n")
}
f.WriteString(indent)
default:
inner := indent + " "
// First and last line shared with bracket:
for i, v := range l {
if i > 0 {
f.WriteString(",\n")
f.WriteString(inner)
}
v.format(f, inner)
}
}
f.WriteByte(']')
}
type ref struct {
id int
}
func (r ref) format(f *formatter, indent string) {
fmt.Fprintf(f, "<see #%d>", f.tagFor(r.id))
}
type target struct {
id int
value node
}
func (t target) format(f *formatter, indent string) {
tag := fmt.Sprintf("<#%d> ", f.tagFor(t.id))
switch {
case f.Diffable, f.Compact:
// no indent changes
default:
indent += strings.Repeat(" ", len(tag))
}
f.WriteString(tag)
t.value.format(f, indent)
}
+4
View File
@@ -0,0 +1,4 @@
coverage:
status:
project: off
patch: off
-33
View File
@@ -1,33 +0,0 @@
language: go
os:
- linux
- osx
addons:
apt:
update: true
go:
- 1.9.x
- 1.10.x
- 1.11.x
- 1.12.x
- 1.13.x
- master
before_install:
- |
if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then
brew update
fi
- go get github.com/smartystreets/goconvey
- go get github.com/mattn/goveralls
- go get golang.org/x/tools/cmd/cover
script:
- $HOME/gopath/bin/goveralls -repotoken 3qJVUE0iQwqnCbmNcDsjYu1nh4J4KIFXx
- go test -race -v . -tags ""
- go test -race -v . -tags "libsqlite3"
- go test -race -v . -tags "sqlite_allow_uri_authority sqlite_app_armor sqlite_foreign_keys sqlite_fts5 sqlite_icu sqlite_introspect sqlite_json sqlite_preupdate_hook sqlite_secure_delete sqlite_see sqlite_stat4 sqlite_trace sqlite_userauth sqlite_vacuum_incr sqlite_vtable sqlite_unlock_notify"
- go test -race -v . -tags "sqlite_vacuum_full"

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