pax_global_header00006660000000000000000000000064145461346070014524gustar00rootroot0000000000000052 comment=4e61dcafb573fcea6fb6e7faa9d12df4c2b69991 golang-github-azuread-microsoft-authentication-extensions-for-go-0.0~git20231002.7e3b8e2/000077500000000000000000000000001454613460700307415ustar00rootroot00000000000000.gitattributes000066400000000000000000000000211454613460700335460ustar00rootroot00000000000000golang-github-azuread-microsoft-authentication-extensions-for-go-0.0~git20231002.7e3b8e2*.go text eol=lf golang-github-azuread-microsoft-authentication-extensions-for-go-0.0~git20231002.7e3b8e2/.github/000077500000000000000000000000001454613460700323015ustar00rootroot00000000000000workflows/000077500000000000000000000000001454613460700342575ustar00rootroot00000000000000golang-github-azuread-microsoft-authentication-extensions-for-go-0.0~git20231002.7e3b8e2/.githubbuild-and-test-cache.yml000066400000000000000000000015571454613460700406670ustar00rootroot00000000000000golang-github-azuread-microsoft-authentication-extensions-for-go-0.0~git20231002.7e3b8e2/.github/workflowsname: Build and Test Cache Module on: pull_request: types: [opened, reopened, synchronize] paths: - 'cache/**' push: branches: [dev, main] paths: - 'cache/**' jobs: build_test: strategy: matrix: os: [macos-latest, ubuntu-latest, windows-latest] runs-on: ${{matrix.os}} steps: - uses: actions/checkout@v3 - uses: actions/setup-go@v4 with: go-version-file: "./cache/go.mod" - name: Build working-directory: ./cache run: go build -x - name: Test working-directory: ./cache run: go test -race -v ./... - name: Lint # lint even when a previous step failed if: ${{!cancelled()}} uses: golangci/golangci-lint-action@v3 with: working-directory: ./cache version: v1.52 args: -v codeql.yml000066400000000000000000000041331454613460700362520ustar00rootroot00000000000000golang-github-azuread-microsoft-authentication-extensions-for-go-0.0~git20231002.7e3b8e2/.github/workflowsname: "CodeQL" on: push: branches: [ "dev", "main" ] pull_request: # The branches below must be a subset of the branches above branches: [ "dev", "main" ] schedule: - cron: '30 9 * * 3' jobs: analyze: name: Analyze runs-on: 'ubuntu-latest' timeout-minutes: 60 permissions: actions: read contents: read security-events: write strategy: fail-fast: false matrix: language: [ 'go' ] steps: - name: Checkout repository uses: actions/checkout@v3 - name: Initialize CodeQL uses: github/codeql-action/init@v2 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. # By default, queries listed here will override any specified in a config file. # Prefix the list here with "+" to use these queries and those in the config file. # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs # queries: security-extended,security-and-quality # Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild uses: github/codeql-action/autobuild@v2 # â„šī¸ Command-line programs to run using the OS shell. # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun # If the Autobuild fails above, remove it and uncomment the following three lines. # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. # - run: | # echo "Run, Build Application using script" # ./location_of_script_within_repo/buildscript.sh - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v2 with: category: "/language:${{matrix.language}}" golang-github-azuread-microsoft-authentication-extensions-for-go-0.0~git20231002.7e3b8e2/.gitignore000066400000000000000000000004151454613460700327310ustar00rootroot00000000000000# Binaries for programs and plugins *.exe *.exe~ *.dll *.so *.dylib # Test binary, built with `go test -c` *.test # Output of the go coverage tool, specifically when used with LiteIDE *.out # Dependency directories (remove the comment below to include it) # vendor/ .golangci.yml000066400000000000000000000001631454613460700332460ustar00rootroot00000000000000golang-github-azuread-microsoft-authentication-extensions-for-go-0.0~git20231002.7e3b8e2linters: enable: - gocritic - gofmt linters-settings: gocritic: enabled-checks: - evalOrder CODE_OF_CONDUCT.md000066400000000000000000000006741454613460700334700ustar00rootroot00000000000000golang-github-azuread-microsoft-authentication-extensions-for-go-0.0~git20231002.7e3b8e2# Microsoft Open Source Code of Conduct This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). Resources: - [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/) - [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) - Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns golang-github-azuread-microsoft-authentication-extensions-for-go-0.0~git20231002.7e3b8e2/LICENSE000066400000000000000000000021651454613460700317520ustar00rootroot00000000000000 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 golang-github-azuread-microsoft-authentication-extensions-for-go-0.0~git20231002.7e3b8e2/README.md000066400000000000000000000025441454613460700322250ustar00rootroot00000000000000# Microsoft Authentication Library (MSAL) Extensions for Go This repository contains extension modules for [Microsoft Authentication Library (MSAL) for Go](https://github.com/AzureAD/microsoft-authentication-library-for-go): - [github.com/AzureAD/microsoft-authentication-extensions-for-go/cache](https://github.com/AzureAD/microsoft-authentication-extensions-for-go/tree/dev/cache): cross-platform persistent caching for MSAL public clients ## 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.opensource.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., status check, 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. golang-github-azuread-microsoft-authentication-extensions-for-go-0.0~git20231002.7e3b8e2/SECURITY.md000066400000000000000000000053341454613460700325370ustar00rootroot00000000000000 ## Security Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://docs.microsoft.com/en-us/previous-versions/tn-archive/cc751383(v=technet.10)), please report it to us as described below. ## Reporting Security Issues **Please do not report security vulnerabilities through public GitHub issues.** Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://msrc.microsoft.com/create-report). If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://www.microsoft.com/en-us/msrc/pgp-key-msrc). You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc). Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) * Full paths of source file(s) related to the manifestation of the issue * The location of the affected source code (tag/branch/commit or direct URL) * Any special configuration required to reproduce the issue * Step-by-step instructions to reproduce the issue * Proof-of-concept or exploit code (if possible) * Impact of the issue, including how an attacker might exploit the issue This information will help us triage your report more quickly. If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://microsoft.com/msrc/bounty) page for more details about our active programs. ## Preferred Languages We prefer all communications to be in English. ## Policy Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://www.microsoft.com/en-us/msrc/cvd). golang-github-azuread-microsoft-authentication-extensions-for-go-0.0~git20231002.7e3b8e2/SUPPORT.md000066400000000000000000000024431454613460700324420ustar00rootroot00000000000000# TODO: The maintainer of this repo has not yet edited this file **REPO OWNER**: Do you want Customer Service & Support (CSS) support for this product/project? - **No CSS support:** Fill out this template with information about how to file issues and get help. - **Yes CSS support:** Fill out an intake form at [aka.ms/spot](https://aka.ms/spot). CSS will work with/help you to determine next steps. More details also available at [aka.ms/onboardsupport](https://aka.ms/onboardsupport). - **Not sure?** Fill out a SPOT intake as though the answer were "Yes". CSS will help you decide. *Then remove this first heading from this SUPPORT.MD file before publishing your repo.* # Support ## How to file issues and get help This project uses GitHub Issues to track bugs and feature requests. Please search the existing issues before filing new issues to avoid duplicates. For new issues, file your bug or feature request as a new Issue. For help and questions about using this project, please **REPO MAINTAINER: INSERT INSTRUCTIONS HERE FOR HOW TO ENGAGE REPO OWNERS OR COMMUNITY FOR HELP. COULD BE A STACK OVERFLOW TAG OR OTHER CHANNEL. WHERE WILL YOU HELP PEOPLE?**. ## Microsoft Support Policy Support for this **PROJECT or PRODUCT** is limited to the resources listed above. golang-github-azuread-microsoft-authentication-extensions-for-go-0.0~git20231002.7e3b8e2/cache/000077500000000000000000000000001454613460700320045ustar00rootroot00000000000000README.md000066400000000000000000000043111454613460700332030ustar00rootroot00000000000000golang-github-azuread-microsoft-authentication-extensions-for-go-0.0~git20231002.7e3b8e2/cache# Microsoft Authentication Library (MSAL) Extensions for Go This module contains a persistent cache for [Microsoft Authentication Library (MSAL) for Go](https://github.com/AzureAD/microsoft-authentication-library-for-go) public client applications such as CLI tools. It isn't recommended for web applications or RPC APIs, in which it can cause scaling and performance problems. The cache supports encrypted storage on Linux, macOS and Windows. The encryption facility depends on the platform: - Linux: [libsecret](https://wiki.gnome.org/Projects/Libsecret) (used as a DBus Secret Service client) - macOS: keychain - Windows: data protection API (DPAPI) See the `accessor` package for more details. The `file` package has a plaintext storage provider to use when encryption isn't possible. > Plaintext storage is dangerous. Bearer tokens are not cryptographically bound to a machine and can be stolen. In particular, the refresh token can be used to get access tokens for many resources. > It's important to warn end-users before falling back to plaintext. End-users should ensure they store the tokens in a secure location (e.g. encrypted disk) and must understand they are responsible for their safety. ## Installation ```sh go get -u github.com/AzureAD/microsoft-authentication-extensions-for-go/cache ``` ## 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.opensource.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., status check, 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. accessor/000077500000000000000000000000001454613460700335275ustar00rootroot00000000000000golang-github-azuread-microsoft-authentication-extensions-for-go-0.0~git20231002.7e3b8e2/cacheaccessor.go000066400000000000000000000005511454613460700356610ustar00rootroot00000000000000golang-github-azuread-microsoft-authentication-extensions-for-go-0.0~git20231002.7e3b8e2/cache/accessor// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. See LICENSE in the project root for license information. package accessor import "context" // Accessor accesses data storage. type Accessor interface { Delete(context.Context) error Read(context.Context) ([]byte, error) Write(context.Context, []byte) error } darwin.go000066400000000000000000000045131454613460700353450ustar00rootroot00000000000000golang-github-azuread-microsoft-authentication-extensions-for-go-0.0~git20231002.7e3b8e2/cache/accessor// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. See LICENSE in the project root for license information. //go:build darwin && cgo // +build darwin,cgo package accessor import ( "context" "errors" "github.com/keybase/go-keychain" ) type option func(*Storage) error // WithAccount sets an optional account name for the keychain item holding cached data. func WithAccount(name string) option { return func(s *Storage) error { s.account = name return nil } } // Storage stores data as a password on the macOS keychain. The keychain must be unlocked before Storage can read // or write data. macOS may not allow keychain access from a headless environment such as an SSH session. type Storage struct { account, service string } // New is the constructor for Storage. "servName" is the service name for the keychain item holding cached data. func New(servName string, opts ...option) (*Storage, error) { if servName == "" { return nil, errors.New("servName can't be empty") } s := Storage{service: servName} for _, o := range opts { if err := o(&s); err != nil { return nil, err } } return &s, nil } // Delete deletes the stored data, if any exists. func (s *Storage) Delete(context.Context) error { err := keychain.DeleteGenericPasswordItem(s.service, s.account) if errors.Is(err, keychain.ErrorItemNotFound) || errors.Is(err, keychain.ErrorNoSuchKeychain) { return nil } return err } // Read returns data stored on the keychain or, if the keychain item doesn't exist, a nil slice and nil error. func (s *Storage) Read(context.Context) ([]byte, error) { data, err := keychain.GetGenericPassword(s.service, s.account, "", "") if err != nil { return nil, err } return data, nil } // Write stores data on the keychain. func (s *Storage) Write(_ context.Context, data []byte) error { pw, err := keychain.GetGenericPassword(s.service, s.account, "", "") if err != nil { return err } item := keychain.NewGenericPassword(s.service, s.account, "", nil, "") if pw == nil { // password not found: add it to the keychain item.SetData(data) err = keychain.AddItem(item) } else { // password found: update its value update := keychain.NewGenericPassword(s.service, s.account, "", data, "") err = keychain.UpdateItem(item, update) } return err } var _ Accessor = (*Storage)(nil) darwin_test.go000066400000000000000000000013031454613460700363760ustar00rootroot00000000000000golang-github-azuread-microsoft-authentication-extensions-for-go-0.0~git20231002.7e3b8e2/cache/accessor// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. See LICENSE in the project root for license information. //go:build darwin && cgo // +build darwin,cgo package accessor import ( "testing" "github.com/stretchr/testify/require" ) func TestWithAccount(t *testing.T) { if !manualTests { t.Skipf("set %s to run this test", msalextManualTest) } account := "account" a, err := New(t.Name(), WithAccount(account)) require.NoError(t, err) expected := []byte("expected") err = a.Write(ctx, expected) require.NoError(t, err) actual, err := a.Read(ctx) require.NoError(t, err) require.Equal(t, expected, actual) require.NoError(t, a.Delete(ctx)) } file/000077500000000000000000000000001454613460700344465ustar00rootroot00000000000000golang-github-azuread-microsoft-authentication-extensions-for-go-0.0~git20231002.7e3b8e2/cache/accessorfile.go000066400000000000000000000030021454613460700357070ustar00rootroot00000000000000golang-github-azuread-microsoft-authentication-extensions-for-go-0.0~git20231002.7e3b8e2/cache/accessor/file// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. See LICENSE in the project root for license information. package file import ( "context" "errors" "os" "path/filepath" "sync" "github.com/AzureAD/microsoft-authentication-extensions-for-go/cache/accessor" ) // Storage stores data in an unencrypted file. type Storage struct { m *sync.RWMutex p string } // New is the constructor for Storage. "p" is the path to the file in which to store data. func New(p string) (*Storage, error) { return &Storage{m: &sync.RWMutex{}, p: p}, nil } // Delete deletes the file, if it exists. func (s *Storage) Delete(context.Context) error { s.m.Lock() defer s.m.Unlock() err := os.Remove(s.p) if errors.Is(err, os.ErrNotExist) { return nil } return err } // Read returns the file's content or, if the file doesn't exist, a nil slice and error. func (s *Storage) Read(context.Context) ([]byte, error) { s.m.RLock() defer s.m.RUnlock() b, err := os.ReadFile(s.p) if errors.Is(err, os.ErrNotExist) { return nil, nil } return b, err } // Write stores data in the file, overwriting any content, and creates the file if necessary. func (s *Storage) Write(ctx context.Context, data []byte) error { s.m.Lock() defer s.m.Unlock() err := os.WriteFile(s.p, data, 0600) if errors.Is(err, os.ErrNotExist) { dir := filepath.Dir(s.p) if err = os.MkdirAll(dir, 0700); err == nil { err = os.WriteFile(s.p, data, 0600) } } return err } var _ accessor.Accessor = (*Storage)(nil) file_test.go000066400000000000000000000026361454613460700367620ustar00rootroot00000000000000golang-github-azuread-microsoft-authentication-extensions-for-go-0.0~git20231002.7e3b8e2/cache/accessor/file// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. See LICENSE in the project root for license information. package file import ( "context" "os" "path/filepath" "testing" "github.com/stretchr/testify/require" ) var ctx = context.Background() func TestReadWriteDelete(t *testing.T) { for _, test := range []struct { desc string initialData, want []byte }{ {desc: "Test when the file exists", initialData: []byte("data"), want: []byte("want")}, {desc: "Test when the file doesn't exist", want: []byte("want")}, } { t.Run(test.desc, func(t *testing.T) { p := filepath.Join(t.TempDir(), t.Name()) if test.initialData != nil { require.NoError(t, os.MkdirAll(filepath.Dir(p), 0700)) f, err := os.OpenFile(p, os.O_CREATE|os.O_EXCL|os.O_RDWR, 0600) require.NoError(t, err) _, err = f.Write(test.initialData) require.NoError(t, err) require.NoError(t, f.Close()) } a, err := New(p) require.NoError(t, err) if test.initialData != nil { actual, err := a.Read(ctx) require.NoError(t, err) require.Equal(t, test.initialData, actual) } cp := make([]byte, len(test.want)) copy(cp, test.want) err = a.Write(ctx, cp) require.NoError(t, err) actual, err := a.Read(ctx) require.NoError(t, err) require.Equal(t, test.want, actual) require.NoError(t, a.Delete(context.Background())) }) } } linux.go000066400000000000000000000226271454613460700352260ustar00rootroot00000000000000golang-github-azuread-microsoft-authentication-extensions-for-go-0.0~git20231002.7e3b8e2/cache/accessor// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. See LICENSE in the project root for license information. //go:build linux // +build linux package accessor /* #cgo LDFLAGS: -ldl #include #include typedef struct { int domain; int code; char *message; } gError; typedef struct { const char *name; int type; } schemaAttribute; typedef struct { char *name; int flags; schemaAttribute attributes[32]; // private fields int r; char *r2; char *r3; char *r4; char *r5; char *r6; char *r7; char *r8; } schema; schema *new_schema(char *name, const char *key1, const char *key2) { int i = 0; schema *s; s = malloc(sizeof(schema)); s->flags = 0; // SECRET_SCHEMA_NONE s->name = name; if (key1 != NULL) { s->attributes[i++] = (schemaAttribute){key1, 0}; // 0 == SECRET_SCHEMA_ATTRIBUTE_STRING } if (key2 != NULL) { s->attributes[i++] = (schemaAttribute){key2, 0}; } s->attributes[i] = (schemaAttribute){NULL, 0}; return s; } // free a gError. f must be a pointer to g_error_free void free_g_error(void *f, gError *err) { void (*fn)(gError *err); fn = (void (*)(gError *err))f; fn(err); } // clear (delete) a secret. f must be a pointer to secret_password_clear_sync // https://gnome.pages.gitlab.gnome.org/libsecret/func.password_clear_sync.html int clear(void *f, schema *sch, void* cancellable, gError **err, char *key1, char *value1, char *key2, char *value2) { int (*fn)(schema *sch, void* cancellable, gError **err, char *key1, char *value1, char *key2, char *value2, ...); fn = (int (*)(schema *sch, void* cancellable, gError **err, char *key1, char *value1, char *key2, char *value2, ...))f; int r = fn(sch, cancellable, err, key1, value1, key2, value2, NULL); return r; } // lookup a password. f must be a pointer to secret_password_lookup_sync // https://gnome.pages.gitlab.gnome.org/libsecret/func.password_lookup_sync.html char *lookup(void *f, schema *sch, void* cancellable, gError **err, char *key1, char *value1, char *key2, char *value2) { char *(*fn)(schema *s, void *cancellable, gError **err, char *attrKey1, char *attrValue1, char *attrKey2, char* attrValue2, ...); fn = (char *(*)(schema *s, void *cancellable, gError **err, char *attrKey1, char *attrValue1, char *attrKey2, char* attrValue2, ...))f; char *r = fn(sch, cancellable, err, key1, value1, key2, value2, NULL); return r; } // store a password. f must be a pointer to secret_password_store_sync // https://gnome.pages.gitlab.gnome.org/libsecret/func.password_store_sync.html int store(void *f, schema *sch, char* collection, char *label, char *password, void* cancellable, gError **err, char *key1, char *value1, char *key2, char *value2) { int (*fn)(schema *s, char *collection, char *label, char *data, void *cancellable, gError **err, ...); fn = (int (*)(schema *s, char *collection, char *label, char *data, void *cancellable, gError **err, ...))f; int r = fn(sch, collection, label, password, cancellable, err, key1, value1, key2, value2, NULL); return r; } */ import "C" import ( "context" "encoding/base64" "errors" "fmt" "runtime" "unsafe" ) const so = "libsecret-1.so" type attribute struct { name, value string } type option func(*Storage) error // WithAttribute adds an attribute to the schema representing the cache. // [Storage] supports up to 2 attributes. func WithAttribute(name, value string) option { return func(s *Storage) error { if len(s.attributes) == 2 { return errors.New("Storage supports up to 2 attributes") } s.attributes = append(s.attributes, attribute{name: name, value: value}) return nil } } // WithLabel sets a label on the schema representing the cache. The default label is "MSALCache". func WithLabel(label string) option { return func(s *Storage) error { s.label = label return nil } } // Storage uses libsecret to store data with a DBus Secret Service such as GNOME Keyring or KDE Wallet. The Service // must be unlocked before Storage can access it. Unlocking typically requires user interaction, and some systems may // be unable to unlock the Service in a headless environment such as an SSH session. type Storage struct { // attributes are key/value pairs on the secret schema attributes []attribute // handle is an opaque handle for libsecret returned by dlopen(). It should be // released via dlclose() when no longer needed so the loader knows when it's // safe to unload libsecret. handle unsafe.Pointer // label of the secret schema label string // clear, freeError, lookup and store are the addresses of libsecret functions clear, freeError, lookup, store unsafe.Pointer // schema identifies the cached data in the secret service schema *C.schema } // New is the constructor for Storage. "name" is the name of the secret schema. func New(name string, opts ...option) (*Storage, error) { s := Storage{label: "MSALCache"} for _, o := range opts { if err := o(&s); err != nil { return nil, err } } n := C.CString(so) defer C.free(unsafe.Pointer(n)) // set the handle and finalizer first so any handle will be // released even when this constructor goes on to return an error s.handle = C.dlopen(n, C.RTLD_LAZY) if s.handle == nil { msg := fmt.Sprintf("encrypted storage isn't possible because the dynamic linker couldn't open %s", so) if e := C.dlerror(); e != nil { msg += fmt.Sprintf(". The underlying error is %q", C.GoString(e)) } return nil, errors.New(msg) } runtime.SetFinalizer(&s, func(s *Storage) { if s.handle != nil { C.dlclose(s.handle) } if s.schema != nil { for _, attr := range s.schema.attributes { if attr.name != nil { C.free(unsafe.Pointer(attr.name)) } } C.free(unsafe.Pointer(s.schema.name)) C.free(unsafe.Pointer(s.schema)) } }) clear, err := s.symbol("secret_password_clear_sync") if err != nil { return nil, err } freeError, err := s.symbol("g_error_free") if err != nil { return nil, err } lookup, err := s.symbol("secret_password_lookup_sync") if err != nil { return nil, err } store, err := s.symbol("secret_password_store_sync") if err != nil { return nil, err } s.clear = clear s.freeError = freeError s.lookup = lookup s.store = store // the first nil terminates the list and libsecret ignores any extras attrs := []*C.char{nil, nil} for i, attr := range s.attributes { // libsecret hangs on to these pointers; the finalizer frees them attrs[i] = C.CString(attr.name) } s.schema = C.new_schema(C.CString(name), attrs[0], attrs[1]) return &s, nil } // Delete deletes the stored data, if any exists. func (s *Storage) Delete(context.Context) error { // the first nil terminates the list and libsecret ignores any extras attrs := []*C.char{nil, nil, nil, nil} for i, attr := range s.attributes { name := C.CString(attr.name) defer C.free(unsafe.Pointer(name)) value := C.CString(attr.value) defer C.free(unsafe.Pointer(value)) attrs[i*2] = name attrs[(i*2)+1] = value } var e *C.gError _ = C.clear(s.clear, s.schema, nil, &e, attrs[0], attrs[1], attrs[2], attrs[3]) if e != nil { defer C.free_g_error(s.freeError, e) return fmt.Errorf("couldn't delete cache data: %q", C.GoString(e.message)) } return nil } // Read returns data stored according to the secret schema or, if no such data exists, a nil slice and nil error. func (s *Storage) Read(context.Context) ([]byte, error) { // the first nil terminates the list and libsecret ignores any extras attrs := []*C.char{nil, nil, nil, nil} for i, attr := range s.attributes { name := C.CString(attr.name) defer C.free(unsafe.Pointer(name)) value := C.CString(attr.value) defer C.free(unsafe.Pointer(value)) attrs[i*2] = name attrs[(i*2)+1] = value } var e *C.gError data := C.lookup(s.lookup, s.schema, nil, &e, attrs[0], attrs[1], attrs[2], attrs[3]) if e != nil { defer C.free_g_error(s.freeError, e) return nil, fmt.Errorf("couldn't read data from secret service: %q", C.GoString(e.message)) } if data == nil { return nil, nil } defer C.free(unsafe.Pointer(data)) result, err := base64.StdEncoding.DecodeString(C.GoString(data)) return result, err } // Write stores cache data. func (s *Storage) Write(_ context.Context, data []byte) error { // the first nil terminates the list and libsecret ignores any extras attrs := []*C.char{nil, nil, nil, nil} for i, attr := range s.attributes { name := C.CString(attr.name) defer C.free(unsafe.Pointer(name)) value := C.CString(attr.value) defer C.free(unsafe.Pointer(value)) attrs[i*2] = name attrs[(i*2)+1] = value } pw := C.CString(base64.StdEncoding.EncodeToString(data)) defer C.free(unsafe.Pointer(pw)) var label *C.char if s.label != "" { label = C.CString(s.label) defer C.free(unsafe.Pointer(label)) } var e *C.gError if r := C.store(s.store, s.schema, nil, label, pw, nil, &e, attrs[0], attrs[1], attrs[2], attrs[3]); r == 0 { msg := "couldn't write data to secret service" if e != nil { defer C.free_g_error(s.freeError, e) if e.message != nil { msg += ": " + C.GoString(e.message) } } return errors.New(msg) } return nil } func (s *Storage) symbol(name string) (unsafe.Pointer, error) { n := C.CString(name) defer C.free(unsafe.Pointer(n)) C.dlerror() fp := C.dlsym(s.handle, n) if e := C.dlerror(); e != nil { return nil, fmt.Errorf("couldn't load %q: %s", name, C.GoString(e)) } return fp, nil } var _ Accessor = (*Storage)(nil) linux_test.go000066400000000000000000000031611454613460700362550ustar00rootroot00000000000000golang-github-azuread-microsoft-authentication-extensions-for-go-0.0~git20231002.7e3b8e2/cache/accessor// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. See LICENSE in the project root for license information. //go:build linux && cgo // +build linux,cgo package accessor import ( "testing" "github.com/stretchr/testify/require" ) func TestTooManyAttributes(t *testing.T) { _, err := New(t.Name(), WithAttribute("", ""), WithAttribute("", ""), WithAttribute("", "")) require.Error(t, err) } func TestWithAttribute(t *testing.T) { if !manualTests { t.Skipf("set %s to run this test", msalextManualTest) } a, err := New(t.Name()) require.NoError(t, err) require.Empty(t, a.attributes) expected := []byte("expected") err = a.Write(ctx, expected) require.NoError(t, err) actual, err := a.Read(ctx) require.NoError(t, err) require.Equal(t, expected, actual) b, err := New(t.Name(), WithAttribute("1", "1")) require.NoError(t, err) require.Equal(t, 1, len(b.attributes)) actual, err = b.Read(ctx) require.NoError(t, err) require.Empty(t, actual) c, err := New(t.Name(), WithAttribute("1", "1"), WithAttribute("2", "2")) require.NoError(t, err) require.Equal(t, 2, len(c.attributes)) actual, err = c.Read(ctx) require.NoError(t, err) require.Empty(t, actual) } func TestWithLabel(t *testing.T) { if !manualTests { t.Skipf("set %s to run this test", msalextManualTest) } label := "label" a, err := New(t.Name(), WithLabel(label)) require.NoError(t, err) require.Equal(t, label, a.label) expected := []byte("expected") err = a.Write(ctx, expected) require.NoError(t, err) actual, err := a.Read(ctx) require.NoError(t, err) require.Equal(t, expected, actual) } storage_test.go000077500000000000000000000025101454613460700365620ustar00rootroot00000000000000golang-github-azuread-microsoft-authentication-extensions-for-go-0.0~git20231002.7e3b8e2/cache/accessor// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. See LICENSE in the project root for license information. //go:build (darwin && cgo) || (linux && cgo) || windows // +build darwin,cgo linux,cgo windows package accessor import ( "context" "os" "path/filepath" "runtime" "testing" "github.com/stretchr/testify/require" ) const msalextManualTest = "MSALEXT_MANUAL_TEST" var ( ctx = context.Background() // the Windows implementation doesn't require user interaction manualTests = runtime.GOOS == "windows" || os.Getenv(msalextManualTest) != "" ) func TestReadWriteDelete(t *testing.T) { if !manualTests { t.Skipf("set %s to run this test", msalextManualTest) } for _, test := range []struct { desc string want []byte }{ {desc: "Test when no stored data exists"}, {desc: "Test writing data then reading it", want: []byte("want")}, } { t.Run(test.desc, func(t *testing.T) { p := filepath.Join(t.TempDir(), t.Name()) a, err := New(p) require.NoError(t, err) if test.want != nil { cp := make([]byte, len(test.want)) copy(cp, test.want) err = a.Write(ctx, cp) require.NoError(t, err) } actual, err := a.Read(ctx) require.NoError(t, err) require.Equal(t, test.want, actual) require.NoError(t, a.Delete(context.Background())) }) } } windows.go000066400000000000000000000053441454613460700355560ustar00rootroot00000000000000golang-github-azuread-microsoft-authentication-extensions-for-go-0.0~git20231002.7e3b8e2/cache/accessor// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. See LICENSE in the project root for license information. //go:build windows // +build windows package accessor import ( "context" "errors" "os" "path/filepath" "sync" "unsafe" "golang.org/x/sys/windows" ) // Storage stores data in a file encrypted by the Windows data protection API. type Storage struct { m *sync.RWMutex p string } // New is the constructor for Storage. "p" is the path to the file in which to store data. func New(p string) (*Storage, error) { return &Storage{m: &sync.RWMutex{}, p: p}, nil } // Delete deletes the file, if it exists. func (s *Storage) Delete(context.Context) error { s.m.Lock() defer s.m.Unlock() err := os.Remove(s.p) if errors.Is(err, os.ErrNotExist) { return nil } return err } // Read returns data from the file. If the file doesn't exist, Read returns a nil slice and error. func (s *Storage) Read(context.Context) ([]byte, error) { s.m.RLock() defer s.m.RUnlock() data, err := os.ReadFile(s.p) if errors.Is(err, os.ErrNotExist) { return nil, nil } if err != nil { return nil, err } if len(data) > 0 { data, err = dpapi(decrypt, data) } return data, err } // Write stores data in the file, creating the file if it doesn't exist. func (s *Storage) Write(_ context.Context, data []byte) error { s.m.Lock() defer s.m.Unlock() data, err := dpapi(encrypt, data) if err != nil { return err } err = os.WriteFile(s.p, data, 0600) if errors.Is(err, os.ErrNotExist) { dir := filepath.Dir(s.p) if err = os.MkdirAll(dir, 0700); err == nil { err = os.WriteFile(s.p, data, 0600) } } return err } type operation int const ( decrypt operation = iota encrypt ) func dpapi(op operation, data []byte) (result []byte, err error) { out := windows.DataBlob{} defer func() { if out.Data != nil { _, e := windows.LocalFree(windows.Handle(unsafe.Pointer(out.Data))) // prefer returning DPAPI errors because they're more interesting than LocalFree errors if e != nil && err == nil { err = e } } }() in := windows.DataBlob{Data: &data[0], Size: uint32(len(data))} switch op { case decrypt: // https://learn.microsoft.com/windows/win32/api/dpapi/nf-dpapi-cryptunprotectdata err = windows.CryptUnprotectData(&in, nil, nil, 0, nil, windows.CRYPTPROTECT_UI_FORBIDDEN, &out) case encrypt: // https://learn.microsoft.com/windows/win32/api/dpapi/nf-dpapi-cryptprotectdata err = windows.CryptProtectData(&in, nil, nil, 0, nil, windows.CRYPTPROTECT_UI_FORBIDDEN, &out) default: err = errors.New("invalid operation") } if err == nil { result = make([]byte, out.Size) copy(result, unsafe.Slice(out.Data, out.Size)) } return result, err } var _ Accessor = (*Storage)(nil) windows_test.go000066400000000000000000000016141454613460700366110ustar00rootroot00000000000000golang-github-azuread-microsoft-authentication-extensions-for-go-0.0~git20231002.7e3b8e2/cache/accessor// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. See LICENSE in the project root for license information. //go:build windows // +build windows package accessor import ( "encoding/json" "os" "path/filepath" "testing" "github.com/stretchr/testify/require" ) func TestWriteEncryption(t *testing.T) { p := filepath.Join(t.TempDir(), t.Name()) a, err := New(p) require.NoError(t, err) data := []byte(`{"key":"value"}`) require.NoError(t, json.Unmarshal(data, &struct{}{}), "test bug: data should unmarshal") require.NoError(t, a.Write(ctx, data)) // Write should have encrypted data before writing it to the file actual, err := os.ReadFile(p) require.NoError(t, err) require.NotEmpty(t, actual) err = json.Unmarshal(actual, &struct{}{}) require.Error(t, err, "Unmarshal should fail because the file's content, being encrypted, isn't JSON") } cache.go000066400000000000000000000110231454613460700333140ustar00rootroot00000000000000golang-github-azuread-microsoft-authentication-extensions-for-go-0.0~git20231002.7e3b8e2/cache// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. See LICENSE in the project root for license information. package cache import ( "context" "errors" "os" "path/filepath" "sync" "time" "github.com/AzureAD/microsoft-authentication-extensions-for-go/cache/accessor" "github.com/AzureAD/microsoft-authentication-extensions-for-go/cache/internal/lock" "github.com/AzureAD/microsoft-authentication-library-for-go/apps/cache" ) var ( // retryDelay lets tests prevent delays when faking errors in Replace retryDelay = 10 * time.Millisecond // timeout lets tests set the default amount of time allowed to read from the accessor timeout = time.Second ) // locker helps tests fake Lock type locker interface { Lock(context.Context) error Unlock() error } // Cache caches authentication data in external storage, using a file lock to coordinate // access with other processes. type Cache struct { // a provides read/write access to storage a accessor.Accessor // data is accessor's data as of the last sync data []byte // l coordinates with other processes l locker // m coordinates this process's goroutines m *sync.Mutex // sync is when this Cache last read from or wrote to a sync time.Time // ts is the path to a file used to timestamp Export and Replace operations ts string } // New is the constructor for Cache. "p" is the path to a file used to track when stored // data changes. [Cache.Export] will create this file and any directories in its path which don't // already exist. func New(a accessor.Accessor, p string) (*Cache, error) { lock, err := lock.New(p+".lockfile", retryDelay) if err != nil { return nil, err } return &Cache{a: a, l: lock, m: &sync.Mutex{}, ts: p}, err } // Export writes the bytes marshaled by "m" to the accessor. // MSAL clients call this method automatically. func (c *Cache) Export(ctx context.Context, m cache.Marshaler, h cache.ExportHints) (err error) { c.m.Lock() defer c.m.Unlock() data, err := m.Marshal() if err != nil { return err } err = c.l.Lock(ctx) if err != nil { return err } defer func() { e := c.l.Unlock() if err == nil { err = e } }() if err = c.a.Write(ctx, data); err == nil { // touch the timestamp file to record the time of this write; discard any // error because this is just an optimization to avoid redundant reads c.sync = time.Now() if er := os.Chtimes(c.ts, c.sync, c.sync); errors.Is(er, os.ErrNotExist) { if er = os.MkdirAll(filepath.Dir(c.ts), 0700); er == nil { f, _ := os.OpenFile(c.ts, os.O_CREATE, 0600) _ = f.Close() } } c.data = data } return err } // Replace reads bytes from the accessor and unmarshals them to "u". // MSAL clients call this method automatically. func (c *Cache) Replace(ctx context.Context, u cache.Unmarshaler, h cache.ReplaceHints) error { c.m.Lock() defer c.m.Unlock() // If the timestamp file indicates cached data hasn't changed since we last read or wrote it, // return c.data, which is the data as of that time. Discard any error from reading the timestamp // because this is just an optimization to prevent unnecessary reads. If we don't know whether // cached data has changed, we assume it has. read := true data := c.data f, err := os.Stat(c.ts) if err == nil { mt := f.ModTime() read = !mt.Equal(c.sync) } if _, hasDeadline := ctx.Deadline(); !hasDeadline { var cancel context.CancelFunc ctx, cancel = context.WithTimeout(ctx, timeout) defer cancel() } // Unmarshal the accessor's data, reading it first if needed. We don't acquire the file lock before // reading from the accessor because it isn't strictly necessary and is relatively expensive. In the // unlikely event that a read overlaps with a write and returns malformed data, Unmarshal will return // an error and we'll try another read. for { if read { data, err = c.a.Read(ctx) if err != nil { break } } err = u.Unmarshal(data) if err == nil { break } else if !read { // c.data is apparently corrupt; Read from the accessor before trying again read = true } select { case <-ctx.Done(): return ctx.Err() case <-time.After(retryDelay): // Unmarshal error; try again } } // Update the sync time only if we read from the accessor and unmarshaled its data. Otherwise // the data hasn't changed since the last read/write, or reading failed and we'll try again on // the next call. if err == nil && read { c.data = data if f, err := os.Stat(c.ts); err == nil { c.sync = f.ModTime() } } return err } var _ cache.ExportReplace = (*Cache)(nil) cache_test.go000066400000000000000000000173731454613460700343710ustar00rootroot00000000000000golang-github-azuread-microsoft-authentication-extensions-for-go-0.0~git20231002.7e3b8e2/cache// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. See LICENSE in the project root for license information. package cache import ( "context" "errors" "fmt" "os" "path/filepath" "runtime" "sync" "testing" "time" "github.com/AzureAD/microsoft-authentication-library-for-go/apps/cache" "github.com/stretchr/testify/require" ) var ctx = context.Background() // fakeExternalCache implements accessor.Accessor to fake a persistent cache type fakeExternalCache struct { data []byte readCallback, writeCallback func() error } func (c *fakeExternalCache) Delete(context.Context) error { c.data = nil return nil } func (a *fakeExternalCache) Read(context.Context) ([]byte, error) { var err error if a.readCallback != nil { err = a.readCallback() } return a.data, err } func (a *fakeExternalCache) Write(ctx context.Context, b []byte) error { var err error if a.writeCallback != nil { err = a.writeCallback() } if err != nil { return err } cp := make([]byte, len(b)) copy(cp, b) a.data = cp return nil } // fakeInternalCache implements cache.Un/Marshaler to fake an MSAL client's in-memory cache type fakeInternalCache struct { data []byte marshalCallback, unmarshalCallback func() error } func (t *fakeInternalCache) Marshal() ([]byte, error) { var err error if t.marshalCallback != nil { err = t.marshalCallback() } return t.data, err } func (t *fakeInternalCache) Unmarshal(b []byte) error { var err error if t.unmarshalCallback != nil { err = t.unmarshalCallback() } cp := make([]byte, len(b)) copy(cp, b) t.data = cp return err } type fakeLock struct { lockErr, unlockErr error } func (l fakeLock) Lock(context.Context) error { return l.lockErr } func (l fakeLock) Unlock() error { return l.unlockErr } func TestExport(t *testing.T) { ec := &fakeExternalCache{} ic := &fakeInternalCache{} p := filepath.Join(t.TempDir(), t.Name(), "ts") c, err := New(ec, p) require.NoError(t, err) // Export should write the in-memory cache to the accessor and touch the timestamp file lastWrite := time.Time{} touched := false for i := 0; i < 3; i++ { s := fmt.Sprint(i) *ic = fakeInternalCache{data: []byte(s)} err = c.Export(ctx, ic, cache.ExportHints{}) require.NoError(t, err) require.Equal(t, []byte(s), ec.data) f, err := os.Stat(p) require.NoError(t, err) mt := f.ModTime() // Two iterations of this loop can run within one unit of system time on Windows, leaving the // modtime apparently unchanged even though Export updated it. On Windows we therefore skip // the strict test, instead requiring only that the modtime change once during this loop. if runtime.GOOS != "windows" { require.NotEqual(t, lastWrite, mt, "Export didn't update the timestamp") } if mt != lastWrite { touched = true } lastWrite = mt } require.True(t, touched, "Export didn't update the timestamp") } func TestFilenameCompat(t *testing.T) { // verify Cache uses the same lock file name as would e.g. the Python implementation p := filepath.Join(t.TempDir(), t.Name()) ec := fakeExternalCache{ // Cache should hold the file lock while calling Write writeCallback: func() error { require.FileExists(t, p+".lockfile", "missing expected lock file") return nil }, } c, err := New(&ec, p) require.NoError(t, err) err = c.Export(ctx, &fakeInternalCache{}, cache.ExportHints{}) require.NoError(t, err) } func TestLockError(t *testing.T) { c, err := New(&fakeExternalCache{}, filepath.Join(t.TempDir(), t.Name())) require.NoError(t, err) expected := errors.New("expected") c.l = fakeLock{lockErr: expected} err = c.Export(ctx, &fakeInternalCache{}, cache.ExportHints{}) require.EqualError(t, err, expected.Error()) } func TestPreservesTimestampFileContent(t *testing.T) { p := filepath.Join(t.TempDir(), t.Name()) expected := []byte("expected") err := os.WriteFile(p, expected, 0600) require.NoError(t, err) ec := fakeExternalCache{} c, err := New(&ec, p) require.NoError(t, err) ic := fakeInternalCache{data: []byte("data")} err = c.Export(ctx, &ic, cache.ExportHints{}) require.NoError(t, err) require.Equal(t, ic.data, ec.data) actual, err := os.ReadFile(p) require.NoError(t, err) require.Equal(t, expected, actual, "Cache truncated, or wrote to, the timestamp file") } func TestRace(t *testing.T) { ic := fakeInternalCache{} ec := fakeExternalCache{} c, err := New(&ec, filepath.Join(t.TempDir(), t.Name())) require.NoError(t, err) wg := sync.WaitGroup{} for i := 0; i < 100; i++ { wg.Add(1) go func(i int) { defer wg.Done() if !t.Failed() { err := c.Replace(ctx, &ic, cache.ReplaceHints{}) if err == nil { err = c.Export(ctx, &ic, cache.ExportHints{}) } if err != nil { t.Errorf("%d: %s", i, err) } } }(i) } wg.Wait() } func TestReplace(t *testing.T) { ic := fakeInternalCache{} ec := fakeExternalCache{} p := filepath.Join(t.TempDir(), t.Name()) c, err := New(&ec, p) require.NoError(t, err) require.Empty(t, ic) // Replace should read data from the accessor (external cache) into the in-memory cache, observing the timestamp file f, err := os.Create(p) require.NoError(t, err) require.NoError(t, f.Close()) for i := uint8(0); i < 4; i++ { ec.data = []byte{i} err = c.Replace(ctx, &ic, cache.ReplaceHints{}) require.NoError(t, err) require.EqualValues(t, ec.data, ic.data) // touch the timestamp file to indicate another accessor wrote data. Backdating ensures the // timestamp changes between iterations even when one executes faster than file time resolution tm := time.Now().Add(-time.Duration(i+1) * time.Second) require.NoError(t, os.Chtimes(p, tm, tm)) } // Replace should return in-memory data when the timestamp indicates no intervening write to the persistent cache for i := 0; i < 4; i++ { err = c.Replace(ctx, &ic, cache.ReplaceHints{}) require.NoError(t, err) // ec.data hasn't changed; ic.data shouldn't change either require.EqualValues(t, ec.data, ic.data) } } func TestReplaceErrors(t *testing.T) { realDelay := retryDelay retryDelay = 0 t.Cleanup(func() { retryDelay = realDelay }) expected := errors.New("expected") t.Run("read", func(t *testing.T) { ec := &fakeExternalCache{readCallback: func() error { return expected }} p := filepath.Join(t.TempDir(), t.Name()) c, err := New(ec, p) require.NoError(t, err) err = c.Replace(ctx, &fakeInternalCache{}, cache.ReplaceHints{}) require.Equal(t, expected, err) }) for _, transient := range []bool{true, false} { name := "unmarshal error" if transient { name = "transient " + name } t.Run(name, func(t *testing.T) { tries := 0 ic := fakeInternalCache{unmarshalCallback: func() error { tries++ if transient && tries > 1 { return nil } return expected }} ec := &fakeExternalCache{} p := filepath.Join(t.TempDir(), t.Name()) c, err := New(ec, p) require.NoError(t, err) cx, cancel := context.WithTimeout(ctx, time.Millisecond) defer cancel() err = c.Replace(cx, &ic, cache.ReplaceHints{}) // err should be nil if the unmarshaling error was transient, non-nil if it wasn't require.Equal(t, transient, err == nil) }) } } func TestUnlockError(t *testing.T) { p := filepath.Join(t.TempDir(), t.Name()) a := fakeExternalCache{} c, err := New(&a, p) require.NoError(t, err) // Export should return an error from Unlock()... unlockErr := errors.New("unlock error") c.l = fakeLock{unlockErr: unlockErr} err = c.Export(ctx, &fakeInternalCache{}, cache.ExportHints{}) require.Equal(t, unlockErr, err) // ...unless another of its calls returned an error writeErr := errors.New("write error") a.writeCallback = func() error { return writeErr } err = c.Export(ctx, &fakeInternalCache{}, cache.ExportHints{}) require.Equal(t, writeErr, err) } example_test.go000066400000000000000000000016621454613460700347530ustar00rootroot00000000000000golang-github-azuread-microsoft-authentication-extensions-for-go-0.0~git20231002.7e3b8e2/cache// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. See LICENSE in the project root for license information. package cache_test import ( "github.com/AzureAD/microsoft-authentication-extensions-for-go/cache" "github.com/AzureAD/microsoft-authentication-extensions-for-go/cache/accessor" "github.com/AzureAD/microsoft-authentication-library-for-go/apps/public" ) // This example shows how to configure an MSAL public client to store data in a peristent, encrypted cache. func Example() { // On Linux and macOS, "s" is an arbitrary name identifying the cache. // On Windows, it's the path to a file in which to store cache data. s := "..." a, err := accessor.New(s) if err != nil { // TODO: handle error } c, err := cache.New(a, s) if err != nil { // TODO: handle error } app, err := public.New("client-id", public.WithCache(c)) if err != nil { // TODO: handle error } _ = app } go.mod000066400000000000000000000014221454613460700330320ustar00rootroot00000000000000golang-github-azuread-microsoft-authentication-extensions-for-go-0.0~git20231002.7e3b8e2/cachemodule github.com/AzureAD/microsoft-authentication-extensions-for-go/cache go 1.18 require ( github.com/AzureAD/microsoft-authentication-library-for-go v1.0.0 github.com/keybase/go-keychain v0.0.0-20230523030712-b5615109f100 github.com/stretchr/testify v1.8.2 golang.org/x/sys v0.8.0 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c ) require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/golang-jwt/jwt/v4 v4.4.3 // indirect github.com/google/uuid v1.3.0 // indirect github.com/kr/pretty v0.2.1 // indirect github.com/kr/text v0.1.0 // indirect github.com/kylelemons/godebug v1.1.0 // indirect github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) go.sum000066400000000000000000000066321454613460700330670ustar00rootroot00000000000000golang-github-azuread-microsoft-authentication-extensions-for-go-0.0~git20231002.7e3b8e2/cachegithub.com/AzureAD/microsoft-authentication-library-for-go v1.0.0 h1:OBhqkivkhkMqLPymWEppkm7vgPQY2XsHoEkaMQ0AdZY= github.com/AzureAD/microsoft-authentication-library-for-go v1.0.0/go.mod h1:kgDmCTgBzIEPFElEF+FK0SdjAor06dRq2Go927dnQ6o= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/golang-jwt/jwt/v4 v4.4.3 h1:Hxl6lhQFj4AnOX6MLrsCb/+7tCj7DxP7VA+2rDIq5AU= github.com/golang-jwt/jwt/v4 v4.4.3/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/keybase/go-keychain v0.0.0-20230523030712-b5615109f100 h1:rG3VnJUnAWyiv7qYmmdOdSapzz6HM+zb9/uRFr0T5EM= github.com/keybase/go-keychain v0.0.0-20230523030712-b5615109f100/go.mod h1:qDHUvIjGZJUtdPtuP4WMu5/U4aVWbFw1MhlkJqCGmCQ= github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU= github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= integration_test.go000066400000000000000000000113121454613460700356340ustar00rootroot00000000000000golang-github-azuread-microsoft-authentication-extensions-for-go-0.0~git20231002.7e3b8e2/cache// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. See LICENSE in the project root for license information. package cache import ( "fmt" "net/http" "path/filepath" "strings" "sync" "testing" "github.com/AzureAD/microsoft-authentication-extensions-for-go/cache/accessor/file" "github.com/AzureAD/microsoft-authentication-library-for-go/apps/confidential" "github.com/AzureAD/microsoft-authentication-library-for-go/apps/public" "github.com/stretchr/testify/require" ) func TestConfidentialClient(t *testing.T) { t.Parallel() p := filepath.Join(t.TempDir(), t.Name()) a, err := file.New(p) require.NoError(t, err) c, err := New(a, p+".timestamp") require.NoError(t, err) cred, err := confidential.NewCredFromSecret("*") require.NoError(t, err) client, err := confidential.New( "https://login.microsoftonline.com/tenant", "clientID", cred, confidential.WithCache(c), confidential.WithHTTPClient(&mockSTS{}), ) require.NoError(t, err) gr := 20 wg := sync.WaitGroup{} for i := 0; i < gr; i++ { wg.Add(1) go func(n int) { defer wg.Done() if t.Failed() { return } s := fmt.Sprint(n) ar, err := client.AcquireTokenByCredential(ctx, []string{s}) switch { case err != nil: t.Error(err) case ar.AccessToken != s: t.Errorf("possible test bug: expected %q from STS, got %q", s, ar.AccessToken) default: ar, err = client.AcquireTokenSilent(ctx, []string{s}) if err != nil { t.Error(err) } else if ar.AccessToken != s { t.Errorf("possible cache corruption: expected %q, got %q", s, ar.AccessToken) } } }(i) } wg.Wait() if t.Failed() { return } // cache should have an access token from each goroutine lost := gr for i := 0; i < gr; i++ { s := fmt.Sprint(i) ar, err := client.AcquireTokenSilent(ctx, []string{s}) if err == nil { lost-- if ar.AccessToken != s { t.Errorf("possible cache corruption: expected %q, got %q", s, ar.AccessToken) } } } require.Equal(t, 0, lost, "lost %d/%d tokens", lost, gr) } func TestPublicClient(t *testing.T) { t.Parallel() p := filepath.Join(t.TempDir(), t.Name()) a, err := file.New(p) require.NoError(t, err) c, err := New(a, p+".timestamp") require.NoError(t, err) sts := mockSTS{} client, err := public.New("clientID", public.WithCache(c), public.WithHTTPClient(&sts)) require.NoError(t, err) gr := 20 wg := sync.WaitGroup{} for i := 0; i < gr; i++ { wg.Add(1) go func(n int) { defer wg.Done() if t.Failed() { return } s := fmt.Sprint(n) ar, err := client.AcquireTokenByUsernamePassword(ctx, []string{s}, s, "password") switch { case err != nil: t.Error(err) case ar.AccessToken != s: t.Errorf("possible test bug: expected %q from STS, got %q", s, ar.AccessToken) default: ar, err = client.AcquireTokenSilent(ctx, []string{s}, public.WithSilentAccount(ar.Account)) if err != nil { t.Error(err) } else if ar.AccessToken != s { t.Errorf("possible cache corruption: expected %q, got %q", s, ar.AccessToken) } } }(i) } wg.Wait() if t.Failed() { return } accounts, err := client.Accounts(ctx) require.NoError(t, err) require.Equal(t, gr, len(accounts), "should have a cached account for each goroutine") // Verify no access token cached above was lost due to a race. Silent auth should return a cached // access token given any scope above. A token request during this loop indicates the client // exchanged a refresh token to reacquire the access token it should have found in the cache. lostATs, reqs := 0, 0 sts.tokenRequestCallback = func(*http.Request) { reqs++ } for _, a := range accounts { s, _, found := strings.Cut(a.HomeAccountID, ".") require.True(t, found, "unexpected home account ID %q", a.HomeAccountID) ar, err := client.AcquireTokenSilent(ctx, []string{s}, public.WithSilentAccount(a)) if err != nil { // the cache has no access token for the expected scope and no refresh token for the account lostATs++ } else if ar.AccessToken != s { t.Errorf("possible cache corruption: expected %q, got %q", s, ar.AccessToken) } } require.Equal(t, 0, lostATs+reqs, "lost %d/%d access tokens", reqs, gr) // The cache has all the expected access tokens but may have lost refresh tokens, so we try silent // auth again for each account, passing a new scope to force the client to use a refresh token. lostRTs := 0 for _, a := range accounts { s := "novelscope" ar, err := client.AcquireTokenSilent(ctx, []string{s}, public.WithSilentAccount(a)) if err != nil { lostRTs++ } else if ar.AccessToken != s { t.Errorf("possible cache corruption: expected %q, got %q", s, ar.AccessToken) } } require.Equal(t, 0, lostRTs, "lost %d/%d refresh tokens", lostRTs, gr) } internal/000077500000000000000000000000001454613460700335415ustar00rootroot00000000000000golang-github-azuread-microsoft-authentication-extensions-for-go-0.0~git20231002.7e3b8e2/cacheflock/000077500000000000000000000000001454613460700346375ustar00rootroot00000000000000golang-github-azuread-microsoft-authentication-extensions-for-go-0.0~git20231002.7e3b8e2/cache/internalLICENSE000066400000000000000000000027071454613460700356520ustar00rootroot00000000000000golang-github-azuread-microsoft-authentication-extensions-for-go-0.0~git20231002.7e3b8e2/cache/internal/flockCopyright (c) 2015-2020, Tim Heckman All rights reserved. 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 gofrs 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 HOLDER 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. README.md000066400000000000000000000004261454613460700361200ustar00rootroot00000000000000golang-github-azuread-microsoft-authentication-extensions-for-go-0.0~git20231002.7e3b8e2/cache/internal/flock# gofrs/flock [gofrs/flock](https://github.com/gofrs/flock) implements a thread-safe sync.Locker interface for file locking. The source code has been copied here as we need to modify this library to expose the underlying lock file handle which is needed in msal extensions. flock.go000066400000000000000000000072431454613460700362720ustar00rootroot00000000000000golang-github-azuread-microsoft-authentication-extensions-for-go-0.0~git20231002.7e3b8e2/cache/internal/flock// Copyright 2015 Tim Heckman. All rights reserved. // Use of this source code is governed by the BSD 3-Clause // license that can be found in the LICENSE file. // Package flock implements a thread-safe interface for file locking. // It also includes a non-blocking TryLock() function to allow locking // without blocking execution. // // Package flock is released under the BSD 3-Clause License. See the LICENSE file // for more details. // // While using this library, remember that the locking behaviors are not // guaranteed to be the same on each platform. For example, some UNIX-like // operating systems will transparently convert a shared lock to an exclusive // lock. If you Unlock() the flock from a location where you believe that you // have the shared lock, you may accidentally drop the exclusive lock. package flock import ( "context" "os" "sync" "time" ) // Flock is the struct type to handle file locking. All fields are unexported, // with access to some of the fields provided by getter methods (Path() and Locked()). type Flock struct { path string m sync.RWMutex fh *os.File l bool r bool } // New returns a new instance of *Flock. The only parameter // it takes is the path to the desired lockfile. func New(path string) *Flock { return &Flock{path: path} } // NewFlock returns a new instance of *Flock. The only parameter // it takes is the path to the desired lockfile. // // Deprecated: Use New instead. func NewFlock(path string) *Flock { return New(path) } // Close is equivalent to calling Unlock. // // This will release the lock and close the underlying file descriptor. // It will not remove the file from disk, that's up to your application. func (f *Flock) Close() error { return f.Unlock() } // Fh returns the lock file func (f *Flock) Fh() *os.File { return f.fh } // Path returns the path as provided in NewFlock(). func (f *Flock) Path() string { return f.path } // Locked returns the lock state (locked: true, unlocked: false). // // Warning: by the time you use the returned value, the state may have changed. func (f *Flock) Locked() bool { f.m.RLock() defer f.m.RUnlock() return f.l } // RLocked returns the read lock state (locked: true, unlocked: false). // // Warning: by the time you use the returned value, the state may have changed. func (f *Flock) RLocked() bool { f.m.RLock() defer f.m.RUnlock() return f.r } func (f *Flock) String() string { return f.path } // TryLockContext repeatedly tries to take an exclusive lock until one of the // conditions is met: TryLock succeeds, TryLock fails with error, or Context // Done channel is closed. func (f *Flock) TryLockContext(ctx context.Context, retryDelay time.Duration) (bool, error) { return tryCtx(ctx, f.TryLock, retryDelay) } // TryRLockContext repeatedly tries to take a shared lock until one of the // conditions is met: TryRLock succeeds, TryRLock fails with error, or Context // Done channel is closed. func (f *Flock) TryRLockContext(ctx context.Context, retryDelay time.Duration) (bool, error) { return tryCtx(ctx, f.TryRLock, retryDelay) } func tryCtx(ctx context.Context, fn func() (bool, error), retryDelay time.Duration) (bool, error) { if ctx.Err() != nil { return false, ctx.Err() } for { if ok, err := fn(); ok || err != nil { return ok, err } select { case <-ctx.Done(): return false, ctx.Err() case <-time.After(retryDelay): // try again } } } func (f *Flock) setFh() error { fh, err := os.OpenFile(f.path, os.O_CREATE|os.O_RDWR, os.FileMode(0600)) if err == nil { f.fh = fh } return err } // ensure the file handle is closed if no lock is held func (f *Flock) ensureFhState() { if !f.l && !f.r && f.fh != nil { f.fh.Close() f.fh = nil } } flock_internal_test.go000066400000000000000000000012451454613460700412210ustar00rootroot00000000000000golang-github-azuread-microsoft-authentication-extensions-for-go-0.0~git20231002.7e3b8e2/cache/internal/flockpackage flock import ( "io/ioutil" "os" "testing" ) func TestInternal(t *testing.T) { tmpFileFh, err := ioutil.TempFile(os.TempDir(), "go-flock-") if err != nil { t.Fatal(err) } tmpFileFh.Close() tmpFile := tmpFileFh.Name() os.Remove(tmpFile) lock := New(tmpFile) locked, err := lock.TryLock() if locked == false || err != nil { t.Fatalf("failed to lock: locked: %t, err: %v", locked, err) } newLock := New(tmpFile) locked, err = newLock.TryLock() if locked != false || err != nil { t.Fatalf("should have failed locking: locked: %t, err: %v", locked, err) } if newLock.fh != nil { t.Fatal("file handle should have been released and be nil") } } flock_test.go000066400000000000000000000157161454613460700373350ustar00rootroot00000000000000golang-github-azuread-microsoft-authentication-extensions-for-go-0.0~git20231002.7e3b8e2/cache/internal/flock// Copyright 2015 Tim Heckman. All rights reserved. // Copyright 2018 The Gofrs. All rights reserved. // Use of this source code is governed by the BSD 3-Clause // license that can be found in the LICENSE file. package flock import ( "context" "io/ioutil" "os" "runtime" "testing" "time" . "gopkg.in/check.v1" ) type TestSuite struct { path string flock *Flock } var _ = Suite(&TestSuite{}) func Test(t *testing.T) { TestingT(t) } func (t *TestSuite) SetUpTest(c *C) { tmpFile, err := ioutil.TempFile(os.TempDir(), "go-flock-") c.Assert(err, IsNil) c.Assert(tmpFile, Not(IsNil)) t.path = tmpFile.Name() defer os.Remove(t.path) tmpFile.Close() t.flock = New(t.path) } func (t *TestSuite) TearDownTest(c *C) { err := t.flock.Unlock() c.Assert(err, IsNil) os.Remove(t.path) } func (t *TestSuite) TestNew(c *C) { f := New(t.path) c.Assert(f, Not(IsNil)) c.Check(f.Path(), Equals, t.path) c.Check(f.Locked(), Equals, false) c.Check(f.RLocked(), Equals, false) } func (t *TestSuite) TestFlock_Path(c *C) { path := t.flock.Path() c.Check(path, Equals, t.path) } func (t *TestSuite) TestFlock_Locked(c *C) { locked := t.flock.Locked() c.Check(locked, Equals, false) } func (t *TestSuite) TestFlock_RLocked(c *C) { locked := t.flock.RLocked() c.Check(locked, Equals, false) } func (t *TestSuite) TestFlock_String(c *C) { str := t.flock.String() c.Assert(str, Equals, t.path) } func (t *TestSuite) TestFlock_TryLock(c *C) { c.Assert(t.flock.Locked(), Equals, false) c.Assert(t.flock.RLocked(), Equals, false) var locked bool var err error locked, err = t.flock.TryLock() c.Assert(err, IsNil) c.Check(locked, Equals, true) c.Check(t.flock.Locked(), Equals, true) c.Check(t.flock.RLocked(), Equals, false) locked, err = t.flock.TryLock() c.Assert(err, IsNil) c.Check(locked, Equals, true) // make sure we just return false with no error in cases // where we would have been blocked locked, err = New(t.path).TryLock() c.Assert(err, IsNil) c.Check(locked, Equals, false) } func (t *TestSuite) TestFlock_TryRLock(c *C) { c.Assert(t.flock.Locked(), Equals, false) c.Assert(t.flock.RLocked(), Equals, false) var locked bool var err error locked, err = t.flock.TryRLock() c.Assert(err, IsNil) c.Check(locked, Equals, true) c.Check(t.flock.Locked(), Equals, false) c.Check(t.flock.RLocked(), Equals, true) locked, err = t.flock.TryRLock() c.Assert(err, IsNil) c.Check(locked, Equals, true) // shared lock should not block. flock2 := New(t.path) locked, err = flock2.TryRLock() c.Assert(err, IsNil) if runtime.GOOS == "aix" { // When using POSIX locks, we can't safely read-lock the same // inode through two different descriptors at the same time: // when the first descriptor is closed, the second descriptor // would still be open but silently unlocked. So a second // TryRLock must return false. c.Check(locked, Equals, false) } else { c.Check(locked, Equals, true) } // make sure we just return false with no error in cases // where we would have been blocked err = t.flock.Unlock() c.Assert(err, IsNil) err = flock2.Unlock() c.Assert(err, IsNil) err = t.flock.Lock() c.Assert(err, IsNil) locked, err = New(t.path).TryRLock() c.Assert(err, IsNil) c.Check(locked, Equals, false) } func (t *TestSuite) TestFlock_TryLockContext(c *C) { // happy path ctx, cancel := context.WithCancel(context.Background()) locked, err := t.flock.TryLockContext(ctx, time.Second) c.Assert(err, IsNil) c.Check(locked, Equals, true) // context already canceled cancel() locked, err = New(t.path).TryLockContext(ctx, time.Second) c.Assert(err, Equals, context.Canceled) c.Check(locked, Equals, false) // timeout ctx, cancel = context.WithTimeout(context.Background(), 10*time.Millisecond) defer cancel() locked, err = New(t.path).TryLockContext(ctx, time.Second) c.Assert(err, Equals, context.DeadlineExceeded) c.Check(locked, Equals, false) } func (t *TestSuite) TestFlock_TryRLockContext(c *C) { // happy path ctx, cancel := context.WithCancel(context.Background()) locked, err := t.flock.TryRLockContext(ctx, time.Second) c.Assert(err, IsNil) c.Check(locked, Equals, true) // context already canceled cancel() locked, err = New(t.path).TryRLockContext(ctx, time.Second) c.Assert(err, Equals, context.Canceled) c.Check(locked, Equals, false) // timeout err = t.flock.Unlock() c.Assert(err, IsNil) err = t.flock.Lock() c.Assert(err, IsNil) ctx, cancel = context.WithTimeout(context.Background(), 10*time.Millisecond) defer cancel() locked, err = New(t.path).TryRLockContext(ctx, time.Second) c.Assert(err, Equals, context.DeadlineExceeded) c.Check(locked, Equals, false) } func (t *TestSuite) TestFlock_Unlock(c *C) { var err error err = t.flock.Unlock() c.Assert(err, IsNil) // get a lock for us to unlock locked, err := t.flock.TryLock() c.Assert(err, IsNil) c.Assert(locked, Equals, true) c.Assert(t.flock.Locked(), Equals, true) c.Check(t.flock.RLocked(), Equals, false) _, err = os.Stat(t.path) c.Assert(os.IsNotExist(err), Equals, false) err = t.flock.Unlock() c.Assert(err, IsNil) c.Check(t.flock.Locked(), Equals, false) c.Check(t.flock.RLocked(), Equals, false) } func (t *TestSuite) TestFlock_Lock(c *C) { c.Assert(t.flock.Locked(), Equals, false) c.Check(t.flock.RLocked(), Equals, false) var err error err = t.flock.Lock() c.Assert(err, IsNil) c.Check(t.flock.Locked(), Equals, true) c.Check(t.flock.RLocked(), Equals, false) // test that the short-circuit works err = t.flock.Lock() c.Assert(err, IsNil) // // Test that Lock() is a blocking call // ch := make(chan error, 2) gf := New(t.path) defer func() { err := gf.Unlock() c.Assert(err, IsNil) }() go func(ch chan<- error) { ch <- nil ch <- gf.Lock() close(ch) }(ch) errCh, ok := <-ch c.Assert(ok, Equals, true) c.Assert(errCh, IsNil) err = t.flock.Unlock() c.Assert(err, IsNil) errCh, ok = <-ch c.Assert(ok, Equals, true) c.Assert(errCh, IsNil) c.Check(t.flock.Locked(), Equals, false) c.Check(t.flock.RLocked(), Equals, false) c.Check(gf.Locked(), Equals, true) c.Check(gf.RLocked(), Equals, false) } func (t *TestSuite) TestFlock_RLock(c *C) { c.Assert(t.flock.Locked(), Equals, false) c.Check(t.flock.RLocked(), Equals, false) var err error err = t.flock.RLock() c.Assert(err, IsNil) c.Check(t.flock.Locked(), Equals, false) c.Check(t.flock.RLocked(), Equals, true) // test that the short-circuit works err = t.flock.RLock() c.Assert(err, IsNil) // // Test that RLock() is a blocking call // ch := make(chan error, 2) gf := New(t.path) defer func() { err := gf.Unlock() c.Assert(err, IsNil) }() go func(ch chan<- error) { ch <- nil ch <- gf.RLock() close(ch) }(ch) errCh, ok := <-ch c.Assert(ok, Equals, true) c.Assert(errCh, IsNil) err = t.flock.Unlock() c.Assert(err, IsNil) errCh, ok = <-ch c.Assert(ok, Equals, true) c.Assert(errCh, IsNil) c.Check(t.flock.Locked(), Equals, false) c.Check(t.flock.RLocked(), Equals, false) c.Check(gf.Locked(), Equals, false) c.Check(gf.RLocked(), Equals, true) } flock_unix.go000066400000000000000000000132301454613460700373260ustar00rootroot00000000000000golang-github-azuread-microsoft-authentication-extensions-for-go-0.0~git20231002.7e3b8e2/cache/internal/flock// Copyright 2015 Tim Heckman. All rights reserved. // Use of this source code is governed by the BSD 3-Clause // license that can be found in the LICENSE file. //go:build !aix && !windows // +build !aix,!windows package flock import ( "os" "syscall" ) // Lock is a blocking call to try and take an exclusive file lock. It will wait // until it is able to obtain the exclusive file lock. It's recommended that // TryLock() be used over this function. This function may block the ability to // query the current Locked() or RLocked() status due to a RW-mutex lock. // // If we are already exclusive-locked, this function short-circuits and returns // immediately assuming it can take the mutex lock. // // If the *Flock has a shared lock (RLock), this may transparently replace the // shared lock with an exclusive lock on some UNIX-like operating systems. Be // careful when using exclusive locks in conjunction with shared locks // (RLock()), because calling Unlock() may accidentally release the exclusive // lock that was once a shared lock. func (f *Flock) Lock() error { return f.lock(&f.l, syscall.LOCK_EX) } // RLock is a blocking call to try and take a shared file lock. It will wait // until it is able to obtain the shared file lock. It's recommended that // TryRLock() be used over this function. This function may block the ability to // query the current Locked() or RLocked() status due to a RW-mutex lock. // // If we are already shared-locked, this function short-circuits and returns // immediately assuming it can take the mutex lock. func (f *Flock) RLock() error { return f.lock(&f.r, syscall.LOCK_SH) } func (f *Flock) lock(locked *bool, flag int) error { f.m.Lock() defer f.m.Unlock() if *locked { return nil } if f.fh == nil { if err := f.setFh(); err != nil { return err } defer f.ensureFhState() } if err := syscall.Flock(int(f.fh.Fd()), flag); err != nil { shouldRetry, reopenErr := f.reopenFDOnError(err) if reopenErr != nil { return reopenErr } if !shouldRetry { return err } if err = syscall.Flock(int(f.fh.Fd()), flag); err != nil { return err } } *locked = true return nil } // Unlock is a function to unlock the file. This file takes a RW-mutex lock, so // while it is running the Locked() and RLocked() functions will be blocked. // // This function short-circuits if we are unlocked already. If not, it calls // syscall.LOCK_UN on the file and closes the file descriptor. It does not // remove the file from disk. It's up to your application to do. // // Please note, if your shared lock became an exclusive lock this may // unintentionally drop the exclusive lock if called by the consumer that // believes they have a shared lock. Please see Lock() for more details. func (f *Flock) Unlock() error { f.m.Lock() defer f.m.Unlock() // if we aren't locked or if the lockfile instance is nil // just return a nil error because we are unlocked if (!f.l && !f.r) || f.fh == nil { return nil } // mark the file as unlocked if err := syscall.Flock(int(f.fh.Fd()), syscall.LOCK_UN); err != nil { return err } f.fh.Close() f.l = false f.r = false f.fh = nil return nil } // TryLock is the preferred function for taking an exclusive file lock. This // function takes an RW-mutex lock before it tries to lock the file, so there is // the possibility that this function may block for a short time if another // goroutine is trying to take any action. // // The actual file lock is non-blocking. If we are unable to get the exclusive // file lock, the function will return false instead of waiting for the lock. If // we get the lock, we also set the *Flock instance as being exclusive-locked. func (f *Flock) TryLock() (bool, error) { return f.try(&f.l, syscall.LOCK_EX) } // TryRLock is the preferred function for taking a shared file lock. This // function takes an RW-mutex lock before it tries to lock the file, so there is // the possibility that this function may block for a short time if another // goroutine is trying to take any action. // // The actual file lock is non-blocking. If we are unable to get the shared file // lock, the function will return false instead of waiting for the lock. If we // get the lock, we also set the *Flock instance as being share-locked. func (f *Flock) TryRLock() (bool, error) { return f.try(&f.r, syscall.LOCK_SH) } func (f *Flock) try(locked *bool, flag int) (bool, error) { f.m.Lock() defer f.m.Unlock() if *locked { return true, nil } if f.fh == nil { if err := f.setFh(); err != nil { return false, err } defer f.ensureFhState() } var retried bool retry: err := syscall.Flock(int(f.fh.Fd()), flag|syscall.LOCK_NB) switch err { case syscall.EWOULDBLOCK: return false, nil case nil: *locked = true return true, nil } if !retried { if shouldRetry, reopenErr := f.reopenFDOnError(err); reopenErr != nil { return false, reopenErr } else if shouldRetry { retried = true goto retry } } return false, err } // reopenFDOnError determines whether we should reopen the file handle // in readwrite mode and try again. This comes from util-linux/sys-utils/flock.c: // // Since Linux 3.4 (commit 55725513) // Probably NFSv4 where flock() is emulated by fcntl(). func (f *Flock) reopenFDOnError(err error) (bool, error) { if err != syscall.EIO && err != syscall.EBADF { return false, nil } if st, err := f.fh.Stat(); err == nil { // if the file is able to be read and written if st.Mode()&0600 == 0600 { f.fh.Close() f.fh = nil // reopen in read-write mode and set the filehandle fh, err := os.OpenFile(f.path, os.O_CREATE|os.O_RDWR, os.FileMode(0600)) if err != nil { return false, err } f.fh = fh return true, nil } } return false, nil } flock_winapi.go000066400000000000000000000036441454613460700376420ustar00rootroot00000000000000golang-github-azuread-microsoft-authentication-extensions-for-go-0.0~git20231002.7e3b8e2/cache/internal/flock// Copyright 2015 Tim Heckman. All rights reserved. // Use of this source code is governed by the BSD 3-Clause // license that can be found in the LICENSE file. //go:build windows // +build windows package flock import ( "syscall" "unsafe" ) var ( kernel32, _ = syscall.LoadLibrary("kernel32.dll") procLockFileEx, _ = syscall.GetProcAddress(kernel32, "LockFileEx") procUnlockFileEx, _ = syscall.GetProcAddress(kernel32, "UnlockFileEx") ) const ( winLockfileFailImmediately = 0x00000001 winLockfileExclusiveLock = 0x00000002 winLockfileSharedLock = 0x00000000 ) // Use of 0x00000000 for the shared lock is a guess based on some the MS Windows // `LockFileEX` docs, which document the `LOCKFILE_EXCLUSIVE_LOCK` flag as: // // > The function requests an exclusive lock. Otherwise, it requests a shared // > lock. // // https://msdn.microsoft.com/en-us/library/windows/desktop/aa365203(v=vs.85).aspx func lockFileEx(handle syscall.Handle, flags uint32, reserved uint32, numberOfBytesToLockLow uint32, numberOfBytesToLockHigh uint32, offset *syscall.Overlapped) (bool, syscall.Errno) { r1, _, errNo := syscall.SyscallN( uintptr(procLockFileEx), uintptr(handle), uintptr(flags), uintptr(reserved), uintptr(numberOfBytesToLockLow), uintptr(numberOfBytesToLockHigh), uintptr(unsafe.Pointer(offset))) if r1 != 1 { if errNo == 0 { return false, syscall.EINVAL } return false, errNo } return true, 0 } func unlockFileEx(handle syscall.Handle, reserved uint32, numberOfBytesToLockLow uint32, numberOfBytesToLockHigh uint32, offset *syscall.Overlapped) (bool, syscall.Errno) { r1, _, errNo := syscall.SyscallN( uintptr(procUnlockFileEx), uintptr(handle), uintptr(reserved), uintptr(numberOfBytesToLockLow), uintptr(numberOfBytesToLockHigh), uintptr(unsafe.Pointer(offset)), 0) if r1 != 1 { if errNo == 0 { return false, syscall.EINVAL } return false, errNo } return true, 0 } flock_windows.go000066400000000000000000000104171454613460700400410ustar00rootroot00000000000000golang-github-azuread-microsoft-authentication-extensions-for-go-0.0~git20231002.7e3b8e2/cache/internal/flock// Copyright 2015 Tim Heckman. All rights reserved. // Use of this source code is governed by the BSD 3-Clause // license that can be found in the LICENSE file. package flock import ( "syscall" ) // ErrorLockViolation is the error code returned from the Windows syscall when a // lock would block and you ask to fail immediately. const ErrorLockViolation syscall.Errno = 0x21 // 33 // Lock is a blocking call to try and take an exclusive file lock. It will wait // until it is able to obtain the exclusive file lock. It's recommended that // TryLock() be used over this function. This function may block the ability to // query the current Locked() or RLocked() status due to a RW-mutex lock. // // If we are already locked, this function short-circuits and returns // immediately assuming it can take the mutex lock. func (f *Flock) Lock() error { return f.lock(&f.l, winLockfileExclusiveLock) } // RLock is a blocking call to try and take a shared file lock. It will wait // until it is able to obtain the shared file lock. It's recommended that // TryRLock() be used over this function. This function may block the ability to // query the current Locked() or RLocked() status due to a RW-mutex lock. // // If we are already locked, this function short-circuits and returns // immediately assuming it can take the mutex lock. func (f *Flock) RLock() error { return f.lock(&f.r, winLockfileSharedLock) } func (f *Flock) lock(locked *bool, flag uint32) error { f.m.Lock() defer f.m.Unlock() if *locked { return nil } if f.fh == nil { if err := f.setFh(); err != nil { return err } defer f.ensureFhState() } if _, errNo := lockFileEx(syscall.Handle(f.fh.Fd()), flag, 0, 1, 0, &syscall.Overlapped{}); errNo > 0 { return errNo } *locked = true return nil } // Unlock is a function to unlock the file. This file takes a RW-mutex lock, so // while it is running the Locked() and RLocked() functions will be blocked. // // This function short-circuits if we are unlocked already. If not, it calls // UnlockFileEx() on the file and closes the file descriptor. It does not remove // the file from disk. It's up to your application to do. func (f *Flock) Unlock() error { f.m.Lock() defer f.m.Unlock() // if we aren't locked or if the lockfile instance is nil // just return a nil error because we are unlocked if (!f.l && !f.r) || f.fh == nil { return nil } // mark the file as unlocked if _, errNo := unlockFileEx(syscall.Handle(f.fh.Fd()), 0, 1, 0, &syscall.Overlapped{}); errNo > 0 { return errNo } f.fh.Close() f.l = false f.r = false f.fh = nil return nil } // TryLock is the preferred function for taking an exclusive file lock. This // function does take a RW-mutex lock before it tries to lock the file, so there // is the possibility that this function may block for a short time if another // goroutine is trying to take any action. // // The actual file lock is non-blocking. If we are unable to get the exclusive // file lock, the function will return false instead of waiting for the lock. If // we get the lock, we also set the *Flock instance as being exclusive-locked. func (f *Flock) TryLock() (bool, error) { return f.try(&f.l, winLockfileExclusiveLock) } // TryRLock is the preferred function for taking a shared file lock. This // function does take a RW-mutex lock before it tries to lock the file, so there // is the possibility that this function may block for a short time if another // goroutine is trying to take any action. // // The actual file lock is non-blocking. If we are unable to get the shared file // lock, the function will return false instead of waiting for the lock. If we // get the lock, we also set the *Flock instance as being shared-locked. func (f *Flock) TryRLock() (bool, error) { return f.try(&f.r, winLockfileSharedLock) } func (f *Flock) try(locked *bool, flag uint32) (bool, error) { f.m.Lock() defer f.m.Unlock() if *locked { return true, nil } if f.fh == nil { if err := f.setFh(); err != nil { return false, err } defer f.ensureFhState() } _, errNo := lockFileEx(syscall.Handle(f.fh.Fd()), flag|winLockfileFailImmediately, 0, 1, 0, &syscall.Overlapped{}) if errNo > 0 { if errNo == ErrorLockViolation || errNo == syscall.ERROR_IO_PENDING { return false, nil } return false, errNo } *locked = true return true, nil } lock/000077500000000000000000000000001454613460700344715ustar00rootroot00000000000000golang-github-azuread-microsoft-authentication-extensions-for-go-0.0~git20231002.7e3b8e2/cache/internallock.go000066400000000000000000000056121454613460700357540ustar00rootroot00000000000000golang-github-azuread-microsoft-authentication-extensions-for-go-0.0~git20231002.7e3b8e2/cache/internal/lock// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. See LICENSE in the project root for license information. package lock import ( "context" "errors" "fmt" "os" "path/filepath" "runtime" "syscall" "time" "github.com/AzureAD/microsoft-authentication-extensions-for-go/cache/internal/flock" ) // timeout lets tests set the default amount of time allowed to acquire the lock var timeout = 5 * time.Second // flocker helps tests fake flock type flocker interface { Fh() *os.File Path() string TryLockContext(context.Context, time.Duration) (bool, error) Unlock() error } // Lock uses a file lock to coordinate access to resources shared with other processes. // Callers are responsible for preventing races within a process. Lock applies advisory // locks on Linux and macOS and is therefore unreliable on these platforms when several // processes concurrently try to acquire the lock. type Lock struct { f flocker retryDelay time.Duration } // New is the constructor for Lock. "p" is the path to the lock file. func New(p string, retryDelay time.Duration) (*Lock, error) { // ensure all dirs in the path exist before flock tries to create the file err := os.MkdirAll(filepath.Dir(p), os.ModePerm) if err != nil { return nil, err } return &Lock{f: flock.New(p), retryDelay: retryDelay}, nil } // Lock acquires the file lock on behalf of the process. The behavior of concurrent // and repeated calls is undefined. For example, Linux may or may not allow goroutines // scheduled on different threads to hold the lock simultaneously. func (l *Lock) Lock(ctx context.Context) error { if _, hasDeadline := ctx.Deadline(); !hasDeadline { var cancel context.CancelFunc ctx, cancel = context.WithTimeout(ctx, timeout) defer cancel() } for { // flock opens the file before locking it and returns errors due to an existing // lock or one acquired by another process after this process has opened the // file. We ignore some errors here because in such cases we want to retry until // the deadline. locked, err := l.f.TryLockContext(ctx, l.retryDelay) if err != nil { if !(errors.Is(err, os.ErrPermission) || isWindowsSharingViolation(err)) { return err } } else if locked { if fh := l.f.Fh(); fh != nil { s := fmt.Sprintf("{%d} {%s}", os.Getpid(), os.Args[0]) _, _ = fh.WriteString(s) } return nil } } } // Unlock releases the lock and deletes the lock file. func (l *Lock) Unlock() error { err := l.f.Unlock() if err == nil { err = os.Remove(l.f.Path()) } // ignore errors caused by another process deleting the file or locking between the above Unlock and Remove if errors.Is(err, os.ErrNotExist) || errors.Is(err, os.ErrPermission) || isWindowsSharingViolation(err) { return nil } return err } func isWindowsSharingViolation(err error) bool { return runtime.GOOS == "windows" && errors.Is(err, syscall.Errno(32)) } lock_test.go000066400000000000000000000060521454613460700370120ustar00rootroot00000000000000golang-github-azuread-microsoft-authentication-extensions-for-go-0.0~git20231002.7e3b8e2/cache/internal/lock// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. See LICENSE in the project root for license information. package lock import ( "bytes" "context" "errors" "io" "os" "path/filepath" "runtime" "testing" "time" "github.com/stretchr/testify/require" ) var ctx = context.Background() type fakeFlock struct { err error p string } func (f fakeFlock) Fh() *os.File { fh, _ := os.Open(f.p) return fh } func (f fakeFlock) Path() string { return f.p } func (f fakeFlock) TryLockContext(context.Context, time.Duration) (bool, error) { return f.err == nil, f.err } func (f fakeFlock) Unlock() error { return f.err } func TestCreatesAndRemovesFile(t *testing.T) { p := filepath.Join(t.TempDir(), "nonexistent", t.Name()) lock, err := New(p, 0) require.NoError(t, err) require.NoFileExists(t, p) err = lock.Lock(ctx) require.NoError(t, err) require.FileExists(t, p, "Lock didn't create the file") buf := bytes.NewBuffer(nil) _, err = io.Copy(buf, lock.f.Fh()) require.NoError(t, err) require.NotEmpty(t, buf, "Lock didn't write debug info to the locked file") err = lock.Unlock() require.NoError(t, err) require.NoFileExists(t, p, "Unlock didn't remove the file") } func TestFileExists(t *testing.T) { p := filepath.Join(t.TempDir(), t.Name()) f, err := os.Create(p) require.NoError(t, err) data := "stuff" _, err = f.WriteString(data) require.NoError(t, err) require.NoError(t, f.Close()) // Lock should succeed when the file exists but isn't locked lock, err := New(p, 0) require.NoError(t, err) err = lock.Lock(ctx) require.NoError(t, err) buf := bytes.NewBuffer(nil) _, err = io.Copy(buf, lock.f.Fh()) require.NoError(t, err) require.NotEqual(t, data, buf, "Lock didn't write debug info to the locked file") require.NoError(t, lock.Unlock()) require.NoFileExists(t, p, "Unlock didn't remove the file") } func TestLockError(t *testing.T) { p := filepath.Join(t.TempDir(), t.Name()) lock, err := New(p, 0) require.NoError(t, err) expected := errors.New("expected") lock.f = fakeFlock{err: expected} require.Equal(t, lock.Lock(ctx), expected) } func TestLockTimeout(t *testing.T) { p := filepath.Join(t.TempDir(), t.Name()) a, err := New(p, 0) require.NoError(t, err) err = a.Lock(ctx) require.NoError(t, err) defer func(d time.Duration) { timeout = d }(timeout) timeout = 0 b, err := New(p, 0) require.NoError(t, err) err = b.Lock(ctx) require.ErrorIs(t, err, context.DeadlineExceeded) require.NoError(t, a.Unlock()) } func TestUnlockErrors(t *testing.T) { p := filepath.Join(t.TempDir(), t.Name()) lock, err := New(p, 0) require.NoError(t, err) err = lock.Lock(ctx) require.NoError(t, err) if runtime.GOOS != "windows" { // Remove would fail on Windows because the file lock is mandatory there require.NoError(t, os.Remove(p)) } // Unlock should return nil even when the lock file has been removed require.NoError(t, lock.Unlock()) expected := errors.New("it didn't work") lock.f = fakeFlock{err: expected} actual := lock.Unlock() require.Equal(t, expected, actual) } mock_test.go000066400000000000000000000103421454613460700342440ustar00rootroot00000000000000golang-github-azuread-microsoft-authentication-extensions-for-go-0.0~git20231002.7e3b8e2/cache// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. See LICENSE in the project root for license information. package cache import ( "bytes" "encoding/base64" "fmt" "io" "net/http" "strings" ) // mockSTS returns mock Azure AD responses so tests don't have to account for MSAL metadata requests type mockSTS struct { tokenRequestCallback func(*http.Request) } func (m *mockSTS) Do(req *http.Request) (*http.Response, error) { res := http.Response{StatusCode: http.StatusOK} switch s := strings.Split(req.URL.Path, "/"); s[len(s)-1] { case "instance": res.Body = io.NopCloser(bytes.NewReader(instanceMetadata("tenant"))) case "openid-configuration": res.Body = io.NopCloser(bytes.NewReader(tenantMetadata("tenant"))) case "token": if m.tokenRequestCallback != nil { m.tokenRequestCallback(req) } if err := req.ParseForm(); err != nil { return nil, err } scope := strings.Split(req.FormValue("scope"), " ")[0] userinfo := "" if upn := req.FormValue("username"); upn != "" { clientinfo := base64.RawStdEncoding.EncodeToString([]byte(fmt.Sprintf(`{"uid":"%s","utid":"utid"}`, upn))) userinfo = fmt.Sprintf(`, "client_info":"%s", "id_token":"x.e30", "refresh_token": "rt"`, clientinfo) } res.Body = io.NopCloser(bytes.NewReader([]byte(fmt.Sprintf(`{"access_token": %q, "expires_in": 3600%s}`, scope, userinfo)))) default: // User realm metadata request paths look like "/common/UserRealm/user@domain". // Matching on the UserRealm segment avoids having to know the UPN. if s[len(s)-2] == "UserRealm" { res.Body = io.NopCloser( strings.NewReader(`{"account_type":"Managed","cloud_audience_urn":"urn","cloud_instance_name":"...","domain_name":"..."}`), ) } else { panic("unexpected request " + req.URL.String()) } } return &res, nil } func (m *mockSTS) CloseIdleConnections() {} func instanceMetadata(tenant string) []byte { return []byte(strings.ReplaceAll(`{ "tenant_discovery_endpoint": "https://login.microsoftonline.com/{tenant}/v2.0/.well-known/openid-configuration", "api-version": "1.1", "metadata": [ { "preferred_network": "login.microsoftonline.com", "preferred_cache": "login.windows.net", "aliases": [ "login.microsoftonline.com", "login.windows.net", "login.microsoft.com", "sts.windows.net" ] } ] }`, "{tenant}", tenant)) } func tenantMetadata(tenant string) []byte { return []byte(strings.ReplaceAll(`{ "token_endpoint": "https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token", "token_endpoint_auth_methods_supported": [ "client_secret_post", "private_key_jwt", "client_secret_basic" ], "jwks_uri": "https://login.microsoftonline.com/{tenant}/discovery/v2.0/keys", "response_modes_supported": [ "query", "fragment", "form_post" ], "subject_types_supported": [ "pairwise" ], "id_token_signing_alg_values_supported": [ "RS256" ], "response_types_supported": [ "code", "id_token", "code id_token", "id_token token" ], "scopes_supported": [ "openid", "profile", "email", "offline_access" ], "issuer": "https://login.microsoftonline.com/{tenant}/v2.0", "request_uri_parameter_supported": false, "userinfo_endpoint": "https://graph.microsoft.com/oidc/userinfo", "authorization_endpoint": "https://login.microsoftonline.com/{tenant}/oauth2/v2.0/authorize", "device_authorization_endpoint": "https://login.microsoftonline.com/{tenant}/oauth2/v2.0/devicecode", "http_logout_supported": true, "frontchannel_logout_supported": true, "end_session_endpoint": "https://login.microsoftonline.com/{tenant}/oauth2/v2.0/logout", "claims_supported": [ "sub", "iss", "cloud_instance_name", "cloud_instance_host_name", "cloud_graph_host_name", "msgraph_host", "aud", "exp", "iat", "auth_time", "acr", "nonce", "preferred_username", "name", "tid", "ver", "at_hash", "c_hash", "email" ], "kerberos_endpoint": "https://login.microsoftonline.com/{tenant}/kerberos", "tenant_region_scope": "NA", "cloud_instance_name": "microsoftonline.com", "cloud_graph_host_name": "graph.windows.net", "msgraph_host": "graph.microsoft.com", "rbac_url": "https://pas.windows.net" }`, "{tenant}", tenant)) } perf_test.go000066400000000000000000000050311454613460700342460ustar00rootroot00000000000000golang-github-azuread-microsoft-authentication-extensions-for-go-0.0~git20231002.7e3b8e2/cache// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. See LICENSE in the project root for license information. package cache import ( "fmt" "path/filepath" "sync" "testing" "github.com/AzureAD/microsoft-authentication-extensions-for-go/cache/accessor/file" "github.com/AzureAD/microsoft-authentication-library-for-go/apps/cache" "github.com/AzureAD/microsoft-authentication-library-for-go/apps/confidential" "github.com/AzureAD/microsoft-authentication-library-for-go/apps/public" "github.com/stretchr/testify/require" ) // this file benchmarks MSAL clients using Cache and file.Accessor func newCache(b *testing.B) *Cache { p := filepath.Join(b.TempDir(), b.Name()) a, err := file.New(p) require.NoError(b, err) c, err := New(a, p+".timestamp") require.NoError(b, err) return c } func BenchmarkConfidentialClient(b *testing.B) { for _, baseline := range []bool{false, true} { name := "file accessor" if baseline { name = "no persistence" } b.Run(name, func(b *testing.B) { var c cache.ExportReplace if !baseline { c = newCache(b) } cred, err := confidential.NewCredFromSecret("*") require.NoError(b, err) client, err := confidential.New( "https://login.microsoftonline.com/tenant", "ID", cred, confidential.WithCache(c), confidential.WithHTTPClient(&mockSTS{}), ) require.NoError(b, err) gr := 10 wg := sync.WaitGroup{} b.ResetTimer() for i := 0; i < b.N; i++ { for i := 0; i < gr; i++ { wg.Add(1) go func(n int) { defer wg.Done() s := fmt.Sprint(n) _, _ = client.AcquireTokenByCredential(ctx, []string{s}) _, _ = client.AcquireTokenSilent(ctx, []string{s}) }(i) } wg.Wait() } }) } } func BenchmarkPublicClient(b *testing.B) { for _, baseline := range []bool{false, true} { name := "file accessor" if baseline { name = "no persistence" } b.Run(name, func(b *testing.B) { var c cache.ExportReplace if !baseline { c = newCache(b) } client, err := public.New("clientID", public.WithCache(c), public.WithHTTPClient(&mockSTS{})) require.NoError(b, err) gr := 10 wg := sync.WaitGroup{} b.ResetTimer() for i := 0; i < b.N; i++ { for i := 0; i < gr; i++ { wg.Add(1) go func(n int) { defer wg.Done() s := fmt.Sprint(n) ar, _ := client.AcquireTokenByUsernamePassword(ctx, []string{s}, s, "password") _, _ = client.AcquireTokenSilent(ctx, []string{s}, public.WithSilentAccount(ar.Account)) }(i) } wg.Wait() } }) } }