├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── documentation.md │ └── feature_request.md └── workflows │ ├── go.yml │ └── golangci-lint.yml ├── .gitignore ├── .golangci.yml ├── CODEOWNERS ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── RELEASES.md ├── SECURITY.md ├── ado └── build_test.yaml ├── apps ├── cache │ └── cache.go ├── confidential │ ├── confidential.go │ ├── confidential_test.go │ └── examples_test.go ├── design │ ├── design.md │ └── release.md ├── errors │ ├── error_design.md │ └── errors.go ├── internal │ ├── base │ │ ├── base.go │ │ ├── base_test.go │ │ └── storage │ │ │ ├── items.go │ │ │ ├── items_test.go │ │ │ ├── partitioned_storage.go │ │ │ ├── partitioned_storage_test.go │ │ │ ├── storage.go │ │ │ ├── storage_test.go │ │ │ └── testdata │ │ │ ├── test_serialized_cache.json │ │ │ ├── v1.0_cache.json │ │ │ └── v1.0_v1.1_cache.json │ ├── exported │ │ └── exported.go │ ├── json │ │ ├── design.md │ │ ├── json.go │ │ ├── json_test.go │ │ ├── mapslice.go │ │ ├── mapslice_test.go │ │ ├── marshal.go │ │ ├── marshal_test.go │ │ ├── struct.go │ │ ├── struct_test.go │ │ └── types │ │ │ └── time │ │ │ └── time.go │ ├── local │ │ ├── server.go │ │ └── server_test.go │ ├── mock │ │ └── mock.go │ ├── oauth │ │ ├── fake │ │ │ └── fake.go │ │ ├── oauth.go │ │ ├── oauth_test.go │ │ ├── ops │ │ │ ├── accesstokens │ │ │ │ ├── accesstokens.go │ │ │ │ ├── accesstokens_test.go │ │ │ │ ├── apptype_string.go │ │ │ │ └── tokens.go │ │ │ ├── authority │ │ │ │ ├── authority.go │ │ │ │ ├── authority_test.go │ │ │ │ └── authorizetype_string.go │ │ │ ├── internal │ │ │ │ ├── comm │ │ │ │ │ ├── comm.go │ │ │ │ │ ├── comm_test.go │ │ │ │ │ └── compress.go │ │ │ │ └── grant │ │ │ │ │ └── grant.go │ │ │ ├── ops.go │ │ │ └── wstrust │ │ │ │ ├── defs │ │ │ │ ├── endpointtype_string.go │ │ │ │ ├── mex_document_definitions.go │ │ │ │ ├── saml_assertion_definitions.go │ │ │ │ ├── version_string.go │ │ │ │ ├── wstrust_endpoint.go │ │ │ │ └── wstrust_mex_document.go │ │ │ │ ├── wstrust.go │ │ │ │ └── wstrust_test.go │ │ └── resolvers.go │ ├── options │ │ └── options.go │ ├── shared │ │ ├── shared.go │ │ └── shared_test.go │ └── version │ │ └── version.go ├── managedidentity │ ├── azure_ml.go │ ├── cloud_shell.go │ ├── managedidentity.go │ ├── managedidentity_test.go │ ├── servicefabric.go │ └── servicefabric_test.go ├── public │ ├── example_test.go │ ├── public.go │ └── public_test.go ├── testdata │ ├── test-cert-chain-reverse.pem │ ├── test-cert-chain.pem │ └── test-cert.pem └── tests │ ├── benchmarks │ └── confidential.go │ ├── devapps │ ├── README.md │ ├── authorization_code_sample.go │ ├── client_certificate_sample.go │ ├── client_secret_sample.go │ ├── confidential_auth_code_sample.go │ ├── confidential_config.json │ ├── config.json │ ├── device_code_flow_sample.go │ ├── main.go │ ├── managedidentity │ │ └── docs │ │ │ └── msi_manual_testing.md │ ├── sample_cache_accessor.go │ ├── sample_utils.go │ ├── serialized_cache.json │ └── username_password_sample.go │ ├── integration │ ├── README.md │ ├── cache_accessor.go │ ├── integration_test.go │ └── serialized_cache_1.1.1.json │ └── performance │ └── performance_test.go ├── changelog.md ├── docs └── managedidentity_public_api.md ├── go.mod └── go.sum /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Please do NOT file bugs without filling in this form. 4 | title: '[Bug] ' 5 | labels: ["untriaged", "needs attention"] 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Which version of MSAL Go are you using?** 11 | Note that to get help, you need to run the latest version. 12 | 13 | 14 | **Where is the issue?** 15 | * Public client 16 | * [ ] Device code flow 17 | * [ ] Username/Password (ROPC grant) 18 | * [ ] Authorization code flow 19 | * Confidential client 20 | * [ ] Authorization code flow 21 | * [ ] Client credentials: 22 | * [ ] client secret 23 | * [ ] client certificate 24 | * Token cache serialization 25 | * [ ] In-memory cache 26 | * Other (please describe) 27 | 28 | **Is this a new or an existing app?** 29 | 34 | 35 | **What version of Go are you using (`go version`)?** 36 | 37 |
38 | $ go version
39 | 
40 | 41 | **What operating system and processor architecture are you using (`go env`)?** 42 | 43 |
go env Output
44 | $ go env
45 | 
46 | 
47 | 48 | **Repro** 49 | 50 | var your = (code) => here; 51 | 52 | **Expected behavior** 53 | A clear and concise description of what you expected to happen (or code). 54 | 55 | **Actual behavior** 56 | A clear and concise description of what happens, e.g. an exception is thrown, UI freezes. 57 | 58 | **Possible solution** 59 | 60 | 61 | **Additional context / logs / screenshots** 62 | Add any other context about the problem here, such as logs and screenshots. 63 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/documentation.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Documentation 3 | about: Suggest a change to the documentation. 4 | title: '[Documentation] ' 5 | labels: documentation 6 | assignees: '' 7 | 8 | --- 9 | 10 | ### Documentation related to component 11 | 12 | 13 | ### Please check all that apply 14 | 15 | - [ ] typo 16 | - [ ] documentation doesn't exist 17 | - [ ] documentation needs clarification 18 | - [ ] error(s) in the example 19 | - [ ] needs an example 20 | 21 | ### Description of the issue 22 | 23 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project. 4 | title: "[Feature Request] " 5 | labels: enhancement, Feature Request 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]. 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | # This guards against unknown PR until a community member vet it and label it. 8 | types: [ labeled ] 9 | 10 | 11 | jobs: 12 | build: 13 | name: Build 14 | 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | go: ["1.21", "1.22"] 20 | 21 | steps: 22 | - name: Set up Go 1.x 23 | uses: actions/setup-go@v2 24 | with: 25 | go-version: ${{ matrix.go }} 26 | id: go 27 | 28 | - name: Check out code into the Go module directory 29 | uses: actions/checkout@v2 30 | 31 | - name: Get dependencies 32 | run: go get -v -t -d ./... 33 | 34 | # designed to only run on linux 35 | # - name: Format Check 36 | # run: if [ $(gofmt -l -s . | wc -l) -ne 0 ]; then echo "fmt failed"; exit 1; fi 37 | 38 | - name: Build 39 | run: go build ./apps/... 40 | 41 | - name: Unit Tests 42 | run: go test -race -short ./apps/cache/... ./apps/confidential/... ./apps/public/... ./apps/internal/... ./apps/managedidentity/... 43 | # Intergration tests runs on ADO 44 | # - name: Integration Tests 45 | # run: go test -race ./apps/tests/integration/... 46 | # env : 47 | # clientId: ${{ secrets.LAB_APP_CLIENT_ID }} 48 | # clientSecret: ${{ secrets.LAB_APP_CLIENT_SECRET }} 49 | # oboConfidentialClientId: ${{ secrets.OBO_CONFIDENTIAL_APP_CLIENT_ID }} 50 | # oboConfidentialClientSecret: ${{ secrets.OBO_CONFIDENTIAL_APP_CLIENT_SECRET }} 51 | # oboPublicClientId: ${{ secrets.OBO_PUBLIC_APP_CLIENT_ID }} 52 | # CI: ${{secrets.ENABLECI}} 53 | -------------------------------------------------------------------------------- /.github/workflows/golangci-lint.yml: -------------------------------------------------------------------------------- 1 | name: golangci-lint 2 | on: 3 | push: 4 | tags: 5 | - v* 6 | branches: 7 | - main 8 | pull_request: 9 | # This guards against unknown PR until a community member vet it and label it. 10 | types: [ labeled ] 11 | 12 | jobs: 13 | golangci: 14 | name: lint 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/setup-go@v3 18 | with: 19 | go-version: "1.20" 20 | - uses: actions/checkout@v3 21 | - name: golangci-lint 22 | uses: golangci/golangci-lint-action@v3 23 | with: 24 | # Required: the version of golangci-lint is required and must be specified without patch version: we always use the latest patch version. 25 | version: v1.51 26 | 27 | # Optional: golangci-lint command line arguments. 28 | # args: --issues-exit-code=0 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | *.golangci.yml 8 | *.swp 9 | *.pprof 10 | 11 | # OSX specific os files 12 | *.DS_Store 13 | 14 | # Test binary, build with `go test -c` 15 | *.test 16 | 17 | # Output of the go coverage tool, specifically when used with LiteIDE 18 | *.out 19 | 20 | pkg/ 21 | github.com/ 22 | golang.org/ 23 | .vscode/ 24 | .idea/ 25 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | linters: 2 | # enabled in addition to default 3 | enable: 4 | - gosec 5 | 6 | issues: 7 | # Excluding configuration per-path, per-linter, per-text and per-source 8 | exclude-rules: 9 | # Exclude some linters from running on tests files. 10 | - path: _test\.go 11 | linters: 12 | - gosec 13 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @bgavrilMS @rayluo 2 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Microsoft Open Source Code of Conduct 2 | 3 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 4 | 5 | Resources: 6 | 7 | - [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/) 8 | - [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) 9 | - Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns 10 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Microsoft Authentication Library for Go welcomes new contributors 2 | 3 | This document will guide you through the process. 4 | 5 | ## Contributor License agreement 6 | 7 | Please visit [https://cla.microsoft.com/](https://cla.microsoft.com/) and sign the Contributor License 8 | Agreement. You only need to do that once. We can not look at your code until you've submitted this request. 9 | 10 | ## FORK 11 | 12 | Fork the project [on GitHub](https://github.com/AzureAD/microsoft-authentication-library-for-go) and check out 13 | your copy. 14 | 15 | Example for MSAL Go: 16 | 17 | ``` 18 | $ git clone git@github.com:username/microsoft-authentication-library-for-go.git 19 | $ cd microsoft-authentication-library-for-go 20 | $ git remote add upstream git@github.com:AzureAD/microsoft-authentication-library-for-go.git 21 | ``` 22 | 23 | ## Setup, Building and Testing 24 | 25 | Please see the [Build & Run](https://github.com/AzureAD/microsoft-authentication-library-for-go/wiki/build-and-test) wiki page. 26 | 27 | ## Decide on which branch to create 28 | 29 | **Bug fixes for the current stable version need to go to 'main' branch.** 30 | 31 | If you need to contribute to a different branch, please contact us first (open an issue). 32 | 33 | All details after this point is standard - make sure your commits have nice messages, and prefer rebase to merge. 34 | 35 | In case of doubt, please open an issue in the [issue tracker](https://github.com/AzureAD/microsoft-authentication-library-for-go/issues). 36 | 37 | Especially do so if you plan to work on a major change in functionality. Nothing is more 38 | frustrating than seeing your hard work go to waste because your vision 39 | does not align with our goals for the SDK. 40 | 41 | ## Branch 42 | 43 | Okay, so you have decided on the proper branch. Create a feature branch 44 | and start hacking: 45 | 46 | ``` 47 | $ git checkout -b my-feature-branch 48 | ``` 49 | 50 | ## Commit 51 | 52 | Make sure git knows your name and email address: 53 | 54 | ``` 55 | $ git config --global user.name "J. Random User" 56 | $ git config --global user.email "j.random.user@example.com" 57 | ``` 58 | 59 | Writing good commit logs is important. A commit log should describe what 60 | changed and why. Follow these guidelines when writing one: 61 | 62 | 1. The first line should be 50 characters or less and contain a short 63 | description of the change prefixed with the name of the changed 64 | subsystem (e.g. "net: add localAddress and localPort to Socket"). 65 | 2. Keep the second line blank. 66 | 3. Wrap all other lines at 72 columns. 67 | 68 | A good commit log looks like this: 69 | 70 | ``` 71 | fix: explaining the commit in one line 72 | 73 | Body of commit message is a few lines of text, explaining things 74 | in more detail, possibly giving some background about the issue 75 | being fixed, etc etc. 76 | 77 | The body of the commit message can be several paragraphs, and 78 | please do proper word-wrap and keep columns shorter than about 79 | 72 characters or so. That way `git log` will show things 80 | nicely even when it is indented. 81 | ``` 82 | 83 | The header line should be meaningful; it is what other people see when they 84 | run `git shortlog` or `git log --oneline`. 85 | 86 | Check the output of `git log --oneline files_that_you_changed` to find out 87 | what directories your changes touch. 88 | 89 | ### Rebase 90 | 91 | Use `git rebase` (not `git merge`) to sync your work from time to time. 92 | 93 | ``` 94 | $ git fetch upstream 95 | $ git rebase upstream/v0.1 # or upstream/main 96 | ``` 97 | 98 | ### Tests 99 | 100 | It's all standard stuff, but please note that you won't be able to run integration tests locally because they connect to a KeyVault to fetch some test users and passwords. The CI will run them for you. 101 | 102 | ### Push 103 | 104 | ``` 105 | $ git push origin my-feature-branch 106 | ``` 107 | 108 | Go to `https://github.com/username/microsoft-authentication-library-for-go` and select your feature branch. Click 109 | the 'Pull Request' button and fill out the form. 110 | 111 | Pull requests are usually reviewed within a few days. If there are comments 112 | to address, apply your changes in a separate commit and push that to your 113 | feature branch. Post a comment in the pull request afterwards; GitHub does 114 | not send out notifications when you add commits. 115 | 116 | [on GitHub]: https://github.com/AzureAD/microsoft-authentication-library-for-go 117 | [issue tracker]: https://github.com/AzureAD/microsoft-authentication-library-for-go/issues 118 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Microsoft Corporation. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE 22 | -------------------------------------------------------------------------------- /RELEASES.md: -------------------------------------------------------------------------------- 1 | # Microsoft Identity SDK Versioning and Servicing FAQ 2 | 3 | We have adopted the semantic versioning flow that is industry standard for OSS projects. It gives the maximum amount of control on what risk you take with what versions. If you know how semantic versioning works with node.js, java, and ruby none of this will be new. 4 | 5 | ## Semantic Versioning and API stability promises 6 | 7 | Microsoft Identity libraries are independent open source libraries that are used by partners both internal and external to Microsoft. As with the rest of Microsoft, we have moved to a rapid iteration model where bugs are fixed daily and new versions are produced as required. To communicate these frequent changes to external partners and customers, we use semantic versioning for all our public Microsoft Identity SDK libraries. This follows the practices of other open source libraries on the internet. This allows us to support our downstream partners which will lock on certain versions for stability purposes, as well as providing for the distribution over NuGet, CocoaPods, and Maven. 8 | 9 | The semantics are: MAJOR.MINOR.PATCH (example 1.1.5) 10 | 11 | We will update our code distributions to use the latest PATCH semantic version number in order to make sure our customers and partners get the latest bug fixes. Downstream partner needs to pull the latest PATCH version. Most partners should try lock on the latest MINOR version number in their builds and accept any updates in the PATCH number. 12 | 13 | Using NuGet, this ensures all 1.1.0 to 1.1.x updates are included when building your code, but not 1.2. 14 | 15 | ``` 16 | 20 | ``` 21 | 22 | | Version | Description | Example | 23 | |:-------:|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------:|:---------------------------------------------------------------------------------------------------------:| 24 | | x.x.x | PATCH version number. Incrementing these numbers is for bug fixes and updates but do not introduce new features. This is used for close partners who build on our platform release (ex. Azure AD Fabric, Office, etc.),In addition, Cocoapods, NuGet, and Maven use this number to deliver the latest release to customers.,This will update frequently (sometimes within the same day),There is no new features, and no regressions or API surface changes. Code will continue to work unless affected by a particular code fix. | ADAL for iOS 1.0.10,(this was a fix for the Storyboard display that was fixed for a specific Office team) | 25 | | x.x | MINOR version numbers. Incrementing these second numbers are for new feature additions that do not impact existing features or introduce regressions. They are purely additive, but may require testing to ensure nothing is impacted.,All x.x.x bug fixes will also roll up in to this number.,There is no regressions or API surface changes. Code will continue to work unless affected by a particular code fix or needs this new feature. | ADAL for iOS 1.1.0,(this added WPJ capability to ADAL, and rolled all the updates from 1.0.0 to 1.0.12) | 26 | | x | MAJOR version numbers. This should be considered a new, supported version of Microsoft Identity SDK and begins the Azure two year support cycle anew. Major new features are introduced and API changes can occur.,This should only be used after a large amount of testing and used only if those features are needed.,We will continue to service MAJOR version numbers with bug fixes up to the two year support cycle. | ADAL for iOS 1.0,(our first official release of ADAL) | 27 | 28 | ## Serviceability 29 | 30 | When we release a new MINOR version, the previous MINOR version is abandoned. 31 | 32 | When we release a new MAJOR version, we will continue to apply bug fixes to the existing features in the previous MAJOR version for up to the 2 year support cycle for Azure. 33 | Example: We release ADALiOS 2.0 in the future which supports unified Auth for AAD and MSA. Later, we then have a fix in Conditional Access for ADALiOS. Since that feature exists both in ADALiOS 1.1 and ADALiOS 2.0, we will fix both. It will roll up in a PATCH number for each. Customers that are still locked down on ADALiOS 1.1 will receive the benefit of this fix. 34 | 35 | ## Microsoft Identity SDKs and Azure Active Directory 36 | 37 | Microsoft Identity SDKs major versions will maintain backwards compatibility with Azure Active Directory web services through the support period. This means that the API surface area defined in a MAJOR version will continue to work for 2 years after release. 38 | 39 | We will respond to bugs quickly from our partners and customers submitted through GitHub and through our private alias (tellaad@microsoft.com) for security issues and update the PATCH version number. We will also submit a change summary for each PATCH number. 40 | Occasionally, there will be security bugs or breaking bugs from our partners that will require an immediate fix and a publish of an update to all partners and customers. When this occurs, we will do an emergency roll up to a PATCH version number and update all our distribution methods to the latest. 41 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Security 4 | 5 | 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/). 6 | 7 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets Microsoft's [Microsoft's definition of a security vulnerability](https://docs.microsoft.com/en-us/previous-versions/tn-archive/cc751383(v=technet.10)) of a security vulnerability, please report it to us as described below. 8 | 9 | ## Reporting Security Issues 10 | 11 | **Please do not report security vulnerabilities through public GitHub issues.** 12 | 13 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://msrc.microsoft.com/create-report). 14 | 15 | 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 the [Microsoft Security Response Center PGP Key page](https://www.microsoft.com/en-us/msrc/pgp-key-msrc). 16 | 17 | 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). 18 | 19 | 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: 20 | 21 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) 22 | * Full paths of source file(s) related to the manifestation of the issue 23 | * The location of the affected source code (tag/branch/commit or direct URL) 24 | * Any special configuration required to reproduce the issue 25 | * Step-by-step instructions to reproduce the issue 26 | * Proof-of-concept or exploit code (if possible) 27 | * Impact of the issue, including how an attacker might exploit the issue 28 | 29 | This information will help us triage your report more quickly. 30 | 31 | 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. 32 | 33 | ## Preferred Languages 34 | 35 | We prefer all communications to be in English. 36 | 37 | ## Policy 38 | 39 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://www.microsoft.com/en-us/msrc/cvd). 40 | 41 | 42 | -------------------------------------------------------------------------------- /ado/build_test.yaml: -------------------------------------------------------------------------------- 1 | trigger: 2 | - main 3 | 4 | pr: 5 | autoCancel: false 6 | branches: 7 | include: 8 | - main 9 | 10 | pool: 11 | vmImage: "ubuntu-latest" 12 | 13 | steps: 14 | - task: GoTool@0 15 | inputs: 16 | version: "1.22.3" 17 | - task: Go@0 18 | inputs: 19 | command: "get" 20 | arguments: "-d -v -t -d ./..." 21 | workingDirectory: "$(System.DefaultWorkingDirectory)" 22 | displayName: "Install dependencies" 23 | - task: Go@0 24 | inputs: 25 | command: "build" 26 | arguments: "./apps/..." 27 | workingDirectory: "$(System.DefaultWorkingDirectory)" 28 | displayName: "Build" 29 | - task: Go@0 30 | inputs: 31 | command: "test" 32 | arguments: "-race -short ./apps/cache/... ./apps/confidential/... ./apps/public/... ./apps/internal/... ./apps/managedidentity/..." 33 | workingDirectory: "$(System.DefaultWorkingDirectory)" 34 | displayName: "Run Unit Tests" 35 | - task: AzureKeyVault@2 36 | displayName: "Connect to Key Vault" 37 | inputs: 38 | azureSubscription: "AuthSdkResourceManager" 39 | KeyVaultName: "msidlabs" 40 | SecretsFilter: "LabAuth,IDLABS-APP-Confidential-Client-Cert-OnPrem" 41 | - task: Bash@3 42 | displayName: Installing certificate 43 | inputs: 44 | targetType: "inline" 45 | script: | 46 | echo $(LabAuth) | base64 -d > $(Build.SourcesDirectory)/cert.pfx 47 | OPENSSL_CONF=/dev/null openssl pkcs12 -in $(Build.SourcesDirectory)/cert.pfx -out $(Build.SourcesDirectory)/cert.pem -nodes -passin pass:'' -legacy 48 | echo "$(IDLABS-APP-Confidential-Client-Cert-OnPrem)" | base64 -d > $(Build.SourcesDirectory)/ccaCert.pfx 49 | OPENSSL_CONF=/dev/null openssl pkcs12 -in $(Build.SourcesDirectory)/ccaCert.pfx -out $(Build.SourcesDirectory)/ccaCert.pem -nodes -passin pass:'' -legacy 50 | 51 | - task: Go@0 52 | inputs: 53 | command: "test" 54 | arguments: "-race ./apps/tests/integration/..." 55 | workingDirectory: "$(System.DefaultWorkingDirectory)" 56 | displayName: "Run Integration Tests" 57 | -------------------------------------------------------------------------------- /apps/cache/cache.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | /* 5 | Package cache allows third parties to implement external storage for caching token data 6 | for distributed systems or multiple local applications access. 7 | 8 | The data stored and extracted will represent the entire cache. Therefore it is recommended 9 | one msal instance per user. This data is considered opaque and there are no guarantees to 10 | implementers on the format being passed. 11 | */ 12 | package cache 13 | 14 | import "context" 15 | 16 | // Marshaler marshals data from an internal cache to bytes that can be stored. 17 | type Marshaler interface { 18 | Marshal() ([]byte, error) 19 | } 20 | 21 | // Unmarshaler unmarshals data from a storage medium into the internal cache, overwriting it. 22 | type Unmarshaler interface { 23 | Unmarshal([]byte) error 24 | } 25 | 26 | // Serializer can serialize the cache to binary or from binary into the cache. 27 | type Serializer interface { 28 | Marshaler 29 | Unmarshaler 30 | } 31 | 32 | // ExportHints are suggestions for storing data. 33 | type ExportHints struct { 34 | // PartitionKey is a suggested key for partitioning the cache 35 | PartitionKey string 36 | } 37 | 38 | // ReplaceHints are suggestions for loading data. 39 | type ReplaceHints struct { 40 | // PartitionKey is a suggested key for partitioning the cache 41 | PartitionKey string 42 | } 43 | 44 | // ExportReplace exports and replaces in-memory cache data. It doesn't support nil Context or 45 | // define the outcome of passing one. A Context without a timeout must receive a default timeout 46 | // specified by the implementor. Retries must be implemented inside the implementation. 47 | type ExportReplace interface { 48 | // Replace replaces the cache with what is in external storage. Implementors should honor 49 | // Context cancellations and return context.Canceled or context.DeadlineExceeded in those cases. 50 | Replace(ctx context.Context, cache Unmarshaler, hints ReplaceHints) error 51 | // Export writes the binary representation of the cache (cache.Marshal()) to external storage. 52 | // This is considered opaque. Context cancellations should be honored as in Replace. 53 | Export(ctx context.Context, cache Marshaler, hints ExportHints) error 54 | } 55 | -------------------------------------------------------------------------------- /apps/confidential/examples_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | package confidential_test 5 | 6 | import ( 7 | "context" 8 | "fmt" 9 | "log" 10 | "os" 11 | 12 | "github.com/AzureAD/microsoft-authentication-library-for-go/apps/confidential" 13 | ) 14 | 15 | // This example demonstrates the general pattern for authenticating with MSAL Go: 16 | // - create a client (only necessary at application start--it's best to reuse client instances) 17 | // - call AcquireTokenSilent() to search for a cached access token 18 | // - if the cache misses, acquire a new token 19 | func Example() { 20 | cred, err := confidential.NewCredFromSecret("client_secret") 21 | if err != nil { 22 | // TODO: handle error 23 | } 24 | client, err := confidential.New("https://login.microsoftonline.com/your_tenant", "client_id", cred) 25 | if err != nil { 26 | // TODO: handle error 27 | } 28 | 29 | scopes := []string{"scope"} 30 | result, err := client.AcquireTokenSilent(context.TODO(), scopes) 31 | if err != nil { 32 | // cache miss, authenticate with another AcquireToken* method 33 | result, err = client.AcquireTokenByCredential(context.TODO(), scopes) 34 | if err != nil { 35 | // TODO: handle error 36 | } 37 | } 38 | 39 | // TODO: use access token 40 | _ = result.AccessToken 41 | } 42 | 43 | func ExampleNewCredFromCert_pem() { 44 | b, err := os.ReadFile("key.pem") 45 | if err != nil { 46 | log.Fatal(err) 47 | } 48 | 49 | // This extracts our public certificates and private key from the PEM file. If it is 50 | // encrypted, the second argument must be password to decode. 51 | certs, priv, err := confidential.CertFromPEM(b, "") 52 | if err != nil { 53 | log.Fatal(err) 54 | } 55 | 56 | cred, err := confidential.NewCredFromCert(certs, priv) 57 | if err != nil { 58 | log.Fatal(err) 59 | } 60 | fmt.Println(cred) // Simply here so cred is used, otherwise won't compile. 61 | } 62 | -------------------------------------------------------------------------------- /apps/design/design.md: -------------------------------------------------------------------------------- 1 | # MSAL Go Design Guide 2 | 3 | Author: John Doak(jdoak@microsoft.com) 4 | Contributors: 5 | - Keegan Caruso(Keegan.Caruso@microsoft.com) 6 | - Joel Hendrix(jhendrix@microsoft.com) 7 | - Santiago Gonzalez(Santiago.Gonzalez@microsoft.com) 8 | - Bogdan Gavril (bogavril@microsoft.com) 9 | 10 | ## History 11 | 12 | The original code submitted for Go MSAL was a translation of either Java or .Net code. This was done as a best effort by an intern who was attempting their first crack at Go. It had a 13 | very interesting structure that didn't fit into Go style and made it difficult to understand or 14 | change. It used global locks, global variables, base type classes (mimicing inheritance), ... 15 | 16 | This probably should have be re-written from scratch, but we decided to try and do it in pieces. 17 | The lesson to be learned from this is that this type of refactor leads to re-writing the code 7 or 8 times instead of once. 18 | 19 | Much of this lead to a re-write where we were not seeing the forrest because of the trees. Every small change would inevitably become some 60 file refactor and have much larger ramifications than intended. 20 | 21 | The work could not be divided up, because the API and the internals were linked across logical 22 | boundaries. 23 | 24 | What has resulted should be a design that divides code into logical layers and splits 25 | the public API from the internal structure. 26 | 27 | ## General Structure 28 | 29 | Public Surface: 30 | ``` 31 | apps/ - Contains all our code 32 | confidential/ - The confidential application API 33 | public/ - The public application API 34 | cache/ - The cache interface that can be implemented to provide persistence cache storage of credentials 35 | ``` 36 | 37 | Internals: 38 | ``` 39 | apps/ 40 | internal/ 41 | client/ - Shared package for common calls that Public and Confidential apps share 42 | json/ - Our own json encoder/decoder for special needs 43 | shared/ - Holds types that need to be in multiple packages and can't be moved into a single one due to import cycles 44 | requests/ - The package to communicate to services to get tokens 45 | ``` 46 | 47 | ### Use of the Go special internal/ directory 48 | 49 | In Go, a directory called internal/ contains packages that should only be used by other packages 50 | rooted at the same location. 51 | 52 | This is documented here: https://golang.org/doc/go1.4#internalpackages 53 | 54 | For example, a package .../a/b/c/internal/d/e/f can be imported only by code in the directory tree rooted at .../a/b/c. It cannot be imported by code in .../a/b/g or in any other repository. 55 | 56 | We use this featurs quite liberally to make clear what is using an internal package. For example: 57 | 58 | ``` 59 | apps/internal/base - Only can be used by packages defined at apps/ 60 | apps/internal/base/internal/storage - Only can be use by package client 61 | ``` 62 | 63 | ## Public API 64 | 65 | The public API will be encapsulated in apps/. apps/ has 3 packages of interest to users: 66 | 67 | - public/ - This is what MSAL calls the Public Application Client (service client) 68 | - confidential/ - This is what MSAL calls the Confidential Application Client (service) 69 | - cache/ - This provides the interfaces that must be implemented to create persistent caches for any MSAL client 70 | 71 | ## Internals 72 | 73 | In this section we will be talking about internal/. 74 | 75 | ### JSON Handling 76 | 77 | JSON must be handled specially in our app. The basics are, if we receive fields that our 78 | structs do not contain, we cannot drop them. We must send them back to the service. 79 | 80 | To handle that, we use our own custom json package that handles this. 81 | 82 | See the design at: [Design](https://github.com/AzureAD/microsoft-authentication-library-for-go/blob/dev/internal/json/design.md) 83 | 84 | ### Backend communication 85 | 86 | Communication to the backends is done via the requests/ package. oauth.Token is the client 87 | for all communication. 88 | 89 | oauth.Token communicates via REST calls that are encapsulated in the ops/ client. 90 | 91 | ## Adding A Feature 92 | 93 | This is the general way to add a new feature to MSAL: 94 | 95 | - Add the REST calls to ops.REST 96 | - Add the higher level manipulations to oauth.Token 97 | - Add your logic to the app/\ and access the services via your oauth.Token 98 | 99 | ## Notable Differences To Other Clients 100 | 101 | ### TBD: Confidential applications needs to handle multiple users without one big cache 102 | 103 | The MSAL caching design is rather simple. These design decisions and the fact that multiple applications in different languages can share a cache mean it cannot be easily changed. 104 | 105 | The entire cache contents of a confidential.Client is read and written on 106 | almost any action to and from an external cache. 107 | 108 | It is not clear to a user that a confidential client should be per user to prevent scaling 109 | problems. 110 | 111 | We cannot change the MSAL cache design at this time, therefore it should be clear that 112 | confidential.Client should be done per user. This must go beyond a simple doc entry 113 | that can be ignored. Its great to say: "we told you in the doc", but that is AFTER a support call. 114 | 115 | TBD ... 116 | 117 | ### Use of x509.Certificate and CertFromPEM() function 118 | 119 | The original version of this package used an thumbprint and a private key to do authorizations 120 | based on a certificate. But there wasn't a real way to get a thumbprint. 121 | 122 | A thumbprint is defined in the Oauth spec, which we had to track down. It is an SHA-1 hash 123 | from the x509 certificate's DER encdoed ASN1 bytes. 124 | 125 | Since the user was going to need the x509, we moved to having the user provide the x509.Certificate 126 | object. 127 | 128 | We wrote the thumbprint creator for the internals. 129 | 130 | Since we also require the private key and it is not straightforward to get, we added a CertFromPEM() 131 | function that will extract the x509.Certificate and private key. We did support encrypted PEM. 132 | 133 | It should be noted that Keyvault stores things in PKCS12 and PEM. Keyvault is not straight forward 134 | in how it works. Frankly, I'm in serious doubt that a regular Go user can get certs out of 135 | Keyvault's Go API. 136 | 137 | Before I began working on MSAL I was re-writing the Keyvault Go API. https://github.com/element-of-surprise/keyvault . It does the right things to extract cers for TLS now. 138 | I was still working on the Cert() API and hadn't exposed the public surface when I stopped. 139 | 140 | Since we have representation from the Go SDK team, we might have them go bridge this problem in 141 | the current implementation using some of that code so its possible for our users to store the 142 | cert in Keyvault. 143 | 144 | ## Logging 145 | 146 | For errors, see [error design](../errors/error_design.md). 147 | 148 | This library does not log personal identifiable information (PII). For a definition of PII, see https://www.microsoft.com/en-us/trust-center/privacy/customer-data-definitions. MSAL Go does not log any of the 3 data categories listed there. 149 | 150 | The library may log information related to your organization, such as tenant id, authority, client id etc. as well as information that cannot be tied to a user such as request correlation id, HTTP status codes etc. 151 | -------------------------------------------------------------------------------- /apps/design/release.md: -------------------------------------------------------------------------------- 1 | # Release Process 2 | 3 | ## Pre-release checks 4 | 5 | 1. Ensure the CI has ran on main 6 | 2. Run Azure SDK's tests 7 | 3. Update the corresponding version in apps/internal/version/version.go 8 | 9 | ``` 10 | git clone github.com/Azure/azure-sdk-for-go --single-branch --depth=1 11 | cd azure-sdk-for-go/sdk/azidentity 12 | go mod edit -replace=github.com/AzureAD/microsoft-authentication-library-for-go="TODO: disk path to MSAL repo" 13 | go mod tidy 14 | go test -v ./... 15 | ``` 16 | -------------------------------------------------------------------------------- /apps/errors/error_design.md: -------------------------------------------------------------------------------- 1 | # MSAL Error Design 2 | 3 | Author: Abhidnya Patil(abhidnya.patil@microsoft.com) 4 | 5 | Contributors: 6 | 7 | - John Doak(jdoak@microsoft.com) 8 | - Keegan Caruso(Keegan.Caruso@microsoft.com) 9 | - Joel Hendrix(jhendrix@microsoft.com) 10 | 11 | ## Background 12 | 13 | Errors in MSAL are intended for app developers to troubleshoot and not for displaying to end-users. 14 | 15 | ### Go error handling vs other MSAL languages 16 | 17 | Most modern languages use exception based errors. Simply put, you "throw" an exception and it must be caught at some routine in the upper stack or it will eventually crash the program. 18 | 19 | Go doesn't use exceptions, instead it relies on multiple return values, one of which can be the builtin error interface type. It is up to the user to decide what to do. 20 | 21 | ### Go custom error types 22 | 23 | Errors can be created in Go by simply using errors.New() or fmt.Errorf() to create an "error". 24 | 25 | Custom errors can be created in multiple ways. One of the more robust ways is simply to satisfy the error interface: 26 | 27 | ```go 28 | type MyCustomErr struct { 29 | Msg string 30 | } 31 | func (m MyCustomErr) Error() string { // This implements "error" 32 | return m.Msg 33 | } 34 | ``` 35 | 36 | ### MSAL Error Goals 37 | 38 | - Provide diagnostics to the user and for tickets that can be used to track down bugs or client misconfigurations 39 | - Detect errors that are transitory and can be retried 40 | - Allow the user to identify certain errors that the program can respond to, such a informing the user for the need to do an enrollment 41 | 42 | ## Implementing Client Side Errors 43 | 44 | Client side errors indicate a misconfiguration or passing of bad arguments that is non-recoverable. Retrying isn't possible. 45 | 46 | These errors can simply be standard Go errors created by errors.New() or fmt.Errorf(). If down the line we need a custom error, we can introduce it, but for now the error messages just need to be clear on what the issue was. 47 | 48 | ## Implementing Service Side Errors 49 | 50 | Service side errors occur when an external RPC responds either with an HTTP error code or returns a message that includes an error. 51 | 52 | These errors can be transitory (please slow down) or permanent (HTTP 404). To provide our diagnostic goals, we require the ability to differentiate these errors from other errors. 53 | 54 | The current implementation includes a specialized type that captures any error from the server: 55 | 56 | ```go 57 | // CallErr represents an HTTP call error. Has a Verbose() method that allows getting the 58 | // http.Request and Response objects. Implements error. 59 | type CallErr struct { 60 | Req *http.Request 61 | Resp *http.Response 62 | Err error 63 | } 64 | 65 | // Errors implements error.Error(). 66 | func (e CallErr) Error() string { 67 | return e.Err.Error() 68 | } 69 | 70 | // Verbose prints a versbose error message with the request or response. 71 | func (e CallErr) Verbose() string { 72 | e.Resp.Request = nil // This brings in a bunch of TLS stuff we don't need 73 | e.Resp.TLS = nil // Same 74 | return fmt.Sprintf("%s:\nRequest:\n%s\nResponse:\n%s", e.Err, prettyConf.Sprint(e.Req), prettyConf.Sprint(e.Resp)) 75 | } 76 | ``` 77 | 78 | A user will always receive the most concise error we provide. They can tell if it is a server side error using Go error package: 79 | 80 | ```go 81 | var callErr CallErr 82 | if errors.As(err, &callErr) { 83 | ... 84 | } 85 | ``` 86 | 87 | We provide a Verbose() function that can retrieve the most verbose message from any error we provide: 88 | 89 | ```go 90 | fmt.Println(errors.Verbose(err)) 91 | ``` 92 | 93 | If further differentiation is required, we can add custom errors that use Go error wrapping on top of CallErr to achieve our diagnostic goals (such as detecting when to retry a call due to transient errors). 94 | 95 | CallErr is always thrown from the comm package (which handles all http requests) and looks similar to: 96 | 97 | ```go 98 | return nil, errors.CallErr{ 99 | Req: req, 100 | Resp: reply, 101 | Err: fmt.Errorf("http call(%s)(%s) error: reply status code was %d:\n%s", req.URL.String(), req.Method, reply.StatusCode, ErrorResponse), //ErrorResponse is the json body extracted from the http response 102 | } 103 | ``` 104 | 105 | ## Future Decisions 106 | 107 | The ability to retry calls needs to have centralized responsibility. Either the user is doing it or the client is doing it. 108 | 109 | If the user should be responsible, our errors package will include a CanRetry() function that will inform the user if the error provided to them is retryable. This is based on the http error code and possibly the type of error that was returned. It would also include a sleep time if the server returned an amount of time to wait. 110 | 111 | Otherwise we will do this internally and retries will be left to us. 112 | -------------------------------------------------------------------------------- /apps/errors/errors.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | package errors 5 | 6 | import ( 7 | "errors" 8 | "fmt" 9 | "io" 10 | "net/http" 11 | "reflect" 12 | "strings" 13 | 14 | "github.com/kylelemons/godebug/pretty" 15 | ) 16 | 17 | var prettyConf = &pretty.Config{ 18 | IncludeUnexported: false, 19 | SkipZeroFields: true, 20 | TrackCycles: true, 21 | Formatter: map[reflect.Type]interface{}{ 22 | reflect.TypeOf((*io.Reader)(nil)).Elem(): func(r io.Reader) string { 23 | b, err := io.ReadAll(r) 24 | if err != nil { 25 | return "could not read io.Reader content" 26 | } 27 | return string(b) 28 | }, 29 | }, 30 | } 31 | 32 | type verboser interface { 33 | Verbose() string 34 | } 35 | 36 | // Verbose prints the most verbose error that the error message has. 37 | func Verbose(err error) string { 38 | build := strings.Builder{} 39 | for { 40 | if err == nil { 41 | break 42 | } 43 | if v, ok := err.(verboser); ok { 44 | build.WriteString(v.Verbose()) 45 | } else { 46 | build.WriteString(err.Error()) 47 | } 48 | err = errors.Unwrap(err) 49 | } 50 | return build.String() 51 | } 52 | 53 | // New is equivalent to errors.New(). 54 | func New(text string) error { 55 | return errors.New(text) 56 | } 57 | 58 | // CallErr represents an HTTP call error. Has a Verbose() method that allows getting the 59 | // http.Request and Response objects. Implements error. 60 | type CallErr struct { 61 | Req *http.Request 62 | // Resp contains response body 63 | Resp *http.Response 64 | Err error 65 | } 66 | 67 | type InvalidJsonErr struct { 68 | Err error 69 | } 70 | 71 | // Errors implements error.Error(). 72 | func (e CallErr) Error() string { 73 | return e.Err.Error() 74 | } 75 | 76 | // Errors implements error.Error(). 77 | func (e InvalidJsonErr) Error() string { 78 | return e.Err.Error() 79 | } 80 | 81 | // Verbose prints a versbose error message with the request or response. 82 | func (e CallErr) Verbose() string { 83 | e.Resp.Request = nil // This brings in a bunch of TLS crap we don't need 84 | e.Resp.TLS = nil // Same 85 | return fmt.Sprintf("%s:\nRequest:\n%s\nResponse:\n%s", e.Err, prettyConf.Sprint(e.Req), prettyConf.Sprint(e.Resp)) 86 | } 87 | 88 | // Is reports whether any error in errors chain matches target. 89 | func Is(err, target error) bool { 90 | return errors.Is(err, target) 91 | } 92 | 93 | // As finds the first error in errors chain that matches target, 94 | // and if so, sets target to that error value and returns true. 95 | // Otherwise, it returns false. 96 | func As(err error, target interface{}) bool { 97 | return errors.As(err, target) 98 | } 99 | -------------------------------------------------------------------------------- /apps/internal/base/storage/testdata/test_serialized_cache.json: -------------------------------------------------------------------------------- 1 | { 2 | "Account": { 3 | "uid.utid-login.windows.net-contoso": { 4 | "username": "John Doe", 5 | "local_account_id": "object1234", 6 | "realm": "contoso", 7 | "environment": "login.windows.net", 8 | "home_account_id": "uid.utid", 9 | "authority_type": "MSSTS" 10 | } 11 | }, 12 | "RefreshToken": { 13 | "uid.utid-login.windows.net-refreshtoken-my_client_id--s2 s1 s3": { 14 | "target": "s2 s1 s3", 15 | "environment": "login.windows.net", 16 | "credential_type": "RefreshToken", 17 | "secret": "a refresh token", 18 | "client_id": "my_client_id", 19 | "home_account_id": "uid.utid" 20 | } 21 | }, 22 | "AccessToken": { 23 | "an-entry": { 24 | "foo": "bar" 25 | }, 26 | "uid.utid-login.windows.net-accesstoken-my_client_id-contoso-s2 s1 s3": { 27 | "environment": "login.windows.net", 28 | "credential_type": "AccessToken", 29 | "secret": "an access token", 30 | "realm": "contoso", 31 | "target": "s2 s1 s3", 32 | "client_id": "my_client_id", 33 | "cached_at": "1000", 34 | "home_account_id": "uid.utid", 35 | "extended_expires_on": "4600", 36 | "expires_on": "4600" 37 | } 38 | }, 39 | "IdToken": { 40 | "uid.utid-login.windows.net-idtoken-my_client_id-contoso-": { 41 | "realm": "contoso", 42 | "environment": "login.windows.net", 43 | "credential_type": "IdToken", 44 | "secret": "header.eyJvaWQiOiAib2JqZWN0MTIzNCIsICJwcmVmZXJyZWRfdXNlcm5hbWUiOiAiSm9obiBEb2UiLCAic3ViIjogInN1YiJ9.signature", 45 | "client_id": "my_client_id", 46 | "home_account_id": "uid.utid" 47 | } 48 | }, 49 | "unknownEntity": {"field1":"1","field2":"whats"}, 50 | "AppMetadata": { 51 | "AppMetadata-login.windows.net-my_client_id": { 52 | "environment": "login.windows.net", 53 | "client_id": "my_client_id" 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /apps/internal/base/storage/testdata/v1.0_cache.json: -------------------------------------------------------------------------------- 1 | { 2 | "Account": { 3 | "uid.utid-login.windows.net-Contoso": { 4 | "username": "John Doe", 5 | "local_account_id": "object1234", 6 | "realm": "contoso", 7 | "environment": "login.windows.net", 8 | "home_account_id": "uid.utid", 9 | "authority_type": "MSSTS" 10 | } 11 | }, 12 | "RefreshToken": { 13 | "uid.utid-login.windows.net-RefreshToken-my_client_id--s2 s1 s3": { 14 | "target": "s2 s1 s3", 15 | "environment": "login.windows.net", 16 | "credential_type": "RefreshToken", 17 | "secret": "a refresh token", 18 | "client_id": "my_client_id", 19 | "home_account_id": "uid.utid" 20 | } 21 | }, 22 | "AccessToken": { 23 | "uid.utid-login.windows.net-AccessToken-my_client_id-contoso-s2 s1 s3": { 24 | "environment": "login.windows.net", 25 | "credential_type": "AccessToken", 26 | "secret": "an access token", 27 | "realm": "contoso", 28 | "target": "s2 s1 s3", 29 | "client_id": "my_client_id", 30 | "cached_at": "1000", 31 | "home_account_id": "uid.utid", 32 | "extended_expires_on": "4600", 33 | "expires_on": "4600" 34 | } 35 | }, 36 | "IdToken": { 37 | "uid.utid-login.windows.net-IdToken-my_client_id-contoso-": { 38 | "realm": "contoso", 39 | "environment": "login.windows.net", 40 | "credential_type": "IdToken", 41 | "secret": "header.eyJvaWQiOiAib2JqZWN0MTIzNCIsICJwcmVmZXJyZWRfdXNlcm5hbWUiOiAiSm9obiBEb2UiLCAic3ViIjogInN1YiJ9.signature", 42 | "client_id": "my_client_id", 43 | "home_account_id": "uid.utid" 44 | } 45 | }, 46 | "AppMetadata": { 47 | "AppMetadata-login.windows.net-my_client_id": { 48 | "environment": "login.windows.net", 49 | "client_id": "my_client_id" 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /apps/internal/base/storage/testdata/v1.0_v1.1_cache.json: -------------------------------------------------------------------------------- 1 | { 2 | "Account": { 3 | "uid.utid-login.windows.net-Contoso": { 4 | "username": "John Doe", 5 | "local_account_id": "wrong value", 6 | "realm": "contoso", 7 | "environment": "login.windows.net", 8 | "home_account_id": "uid.utid", 9 | "authority_type": "MSSTS" 10 | }, 11 | "uid.utid-login.windows.net-contoso": { 12 | "username": "John Doe", 13 | "local_account_id": "object1234", 14 | "realm": "contoso", 15 | "environment": "login.windows.net", 16 | "home_account_id": "uid.utid", 17 | "authority_type": "MSSTS" 18 | } 19 | }, 20 | "RefreshToken": { 21 | "uid.utid-login.windows.net-RefreshToken-my_client_id--s2 s1 s3": { 22 | "target": "s2 s1 s3", 23 | "environment": "login.windows.net", 24 | "credential_type": "RefreshToken", 25 | "secret": "wrong value", 26 | "client_id": "my_client_id", 27 | "home_account_id": "uid.utid" 28 | }, 29 | "uid.utid-login.windows.net-refreshtoken-my_client_id--s2 s1 s3": { 30 | "target": "s2 s1 s3", 31 | "environment": "login.windows.net", 32 | "credential_type": "RefreshToken", 33 | "secret": "a refresh token", 34 | "client_id": "my_client_id", 35 | "home_account_id": "uid.utid" 36 | } 37 | }, 38 | "AccessToken": { 39 | "uid.utid-login.windows.net-accesstoken-my_client_id-contoso-s2 s1 s3": { 40 | "environment": "login.windows.net", 41 | "credential_type": "AccessToken", 42 | "secret": "an access token", 43 | "realm": "contoso", 44 | "target": "s2 s1 s3", 45 | "client_id": "my_client_id", 46 | "cached_at": "1000", 47 | "home_account_id": "uid.utid", 48 | "extended_expires_on": "4600", 49 | "expires_on": "4600" 50 | }, 51 | "uid.utid-login.windows.net-AccessToken-my_client_id-contoso-s2 s1 s3": { 52 | "environment": "login.windows.net", 53 | "credential_type": "AccessToken", 54 | "secret": "wrong value", 55 | "realm": "contoso", 56 | "target": "s2 s1 s3", 57 | "client_id": "my_client_id", 58 | "cached_at": "1000", 59 | "home_account_id": "uid.utid", 60 | "extended_expires_on": "4600", 61 | "expires_on": "4600" 62 | } 63 | }, 64 | "IdToken": { 65 | "uid.utid-login.windows.net-IdToken-my_client_id-contoso-": { 66 | "realm": "contoso", 67 | "environment": "login.windows.net", 68 | "credential_type": "IdToken", 69 | "secret": "wrong value", 70 | "client_id": "my_client_id", 71 | "home_account_id": "uid.utid" 72 | }, 73 | "uid.utid-login.windows.net-idtoken-my_client_id-contoso-": { 74 | "realm": "contoso", 75 | "environment": "login.windows.net", 76 | "credential_type": "IdToken", 77 | "secret": "header.eyJvaWQiOiAib2JqZWN0MTIzNCIsICJwcmVmZXJyZWRfdXNlcm5hbWUiOiAiSm9obiBEb2UiLCAic3ViIjogInN1YiJ9.signature", 78 | "client_id": "my_client_id", 79 | "home_account_id": "uid.utid" 80 | } 81 | }, 82 | "AppMetadata": { 83 | "AppMetadata-login.windows.net-my_client_id": { 84 | "environment": "login.windows.net", 85 | "client_id": "my_client_id" 86 | }, 87 | "appmetadata-login.windows.net-my_client_id": { 88 | "environment": "login.windows.net", 89 | "client_id": "my_client_id" 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /apps/internal/exported/exported.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | // package exported contains internal types that are re-exported from a public package 5 | package exported 6 | 7 | // AssertionRequestOptions has information required to generate a client assertion 8 | type AssertionRequestOptions struct { 9 | // ClientID identifies the application for which an assertion is requested. Used as the assertion's "iss" and "sub" claims. 10 | ClientID string 11 | 12 | // TokenEndpoint is the intended token endpoint. Used as the assertion's "aud" claim. 13 | TokenEndpoint string 14 | } 15 | 16 | // TokenProviderParameters is the authentication parameters passed to token providers 17 | type TokenProviderParameters struct { 18 | // Claims contains any additional claims requested for the token 19 | Claims string 20 | // CorrelationID of the authentication request 21 | CorrelationID string 22 | // Scopes requested for the token 23 | Scopes []string 24 | // TenantID identifies the tenant in which to authenticate 25 | TenantID string 26 | } 27 | 28 | // TokenProviderResult is the authentication result returned by custom token providers 29 | type TokenProviderResult struct { 30 | // AccessToken is the requested token 31 | AccessToken string 32 | // ExpiresInSeconds is the lifetime of the token in seconds 33 | ExpiresInSeconds int 34 | // RefreshInSeconds indicates the suggested time to refresh the token, if any 35 | RefreshInSeconds int 36 | } 37 | -------------------------------------------------------------------------------- /apps/internal/json/design.md: -------------------------------------------------------------------------------- 1 | # JSON Package Design 2 | Author: John Doak(jdoak@microsoft.com) 3 | 4 | ## Why? 5 | 6 | This project needs a special type of marshal/unmarshal not directly supported 7 | by the encoding/json package. 8 | 9 | The need revolves around a few key wants/needs: 10 | - unmarshal and marshal structs representing JSON messages 11 | - fields in the messgage not in the struct must be maintained when unmarshalled 12 | - those same fields must be marshalled back when encoded again 13 | 14 | The initial version used map[string]interface{} to put in the keys that 15 | were known and then any other keys were put into a field called AdditionalFields. 16 | 17 | This has a few negatives: 18 | - Dual marshaling/unmarshalling is required 19 | - Adding a struct field requires manually adding a key by name to be encoded/decoded from the map (which is a loosely coupled construct), which can lead to bugs that aren't detected or have bad side effects 20 | - Tests can become quickly disconnected if those keys aren't put 21 | in tests as well. So you think you have support working, but you 22 | don't. Existing tests were found that didn't test the marshalling output. 23 | - There is no enforcement that if AdditionalFields is required on one struct, it should be on all containers 24 | that don't have custom marshal/unmarshal. 25 | 26 | This package aims to support our needs by providing custom Marshal()/Unmarshal() functions. 27 | 28 | This prevents all the negatives in the initial solution listed above. However, it does add its own negative: 29 | - Custom encoding/decoding via reflection is messy (as can be seen in encoding/json itself) 30 | 31 | Go proverb: Reflection is never clear 32 | Suggested reading: https://blog.golang.org/laws-of-reflection 33 | 34 | ## Important design decisions 35 | 36 | - We don't want to understand all JSON decoding rules 37 | - We don't want to deal with all the quoting, commas, etc on decode 38 | - Need support for json.Marshaler/Unmarshaler, so we can support types like time.Time 39 | - If struct does not implement json.Unmarshaler, it must have AdditionalFields defined 40 | - We only support root level objects that are \*struct or struct 41 | 42 | To faciliate these goals, we will utilize the json.Encoder and json.Decoder. 43 | They provide streaming processing (efficient) and return errors on bad JSON. 44 | 45 | Support for json.Marshaler/Unmarshaler allows for us to use non-basic types 46 | that must be specially encoded/decoded (like time.Time objects). 47 | 48 | We don't support types that can't customer unmarshal or have AdditionalFields 49 | in order to prevent future devs from forgetting that important field and 50 | generating bad return values. 51 | 52 | Support for root level objects of \*struct or struct simply acknowledges the 53 | fact that this is designed only for the purposes listed in the Introduction. 54 | Outside that (like encoding a lone number) should be done with the 55 | regular json package (as it will not have additional fields). 56 | 57 | We don't support a few things on json supported reference types and structs: 58 | - \*map: no need for pointers to maps 59 | - \*slice: no need for pointers to slices 60 | - any further pointers on struct after \*struct 61 | 62 | There should never be a need for this in Go. 63 | 64 | ## Design 65 | 66 | ## State Machines 67 | 68 | This uses state machine designs that based upon the Rob Pike talk on 69 | lexers and parsers: https://www.youtube.com/watch?v=HxaD_trXwRE 70 | 71 | This is the most common pattern for state machines in Go and 72 | the model to follow closesly when dealing with streaming 73 | processing of textual data. 74 | 75 | Our state machines are based on the type: 76 | ```go 77 | type stateFn func() (stateFn, error) 78 | ``` 79 | 80 | The state machine itself is simply a struct that has methods that 81 | satisfy stateFn. 82 | 83 | Our state machines have a few standard calls 84 | - run(): runs the state machine 85 | - start(): always the first stateFn to be called 86 | 87 | All state machines have the following logic: 88 | * run() is called 89 | * start() is called and returns the next stateFn or error 90 | * stateFn is called 91 | - If returned stateFn(next state) is non-nil, call it 92 | - If error is non-nil, run() returns the error 93 | - If stateFn == nil and err == nil, run() return err == nil 94 | 95 | ## Supporting types 96 | 97 | Marshalling/Unmarshalling must support(within top level struct): 98 | - struct 99 | - \*struct 100 | - []struct 101 | - []\*struct 102 | - []map[string]structContainer 103 | - [][]structContainer 104 | 105 | **Term note:** structContainer == type that has a struct or \*struct inside it 106 | 107 | We specifically do not support []interface or map[string]interface 108 | where the interface value would hold some value with a struct in it. 109 | 110 | Those will still marshal/unmarshal, but without support for 111 | AdditionalFields. 112 | 113 | ## Marshalling 114 | 115 | The marshalling design will be based around a statemachine design. 116 | 117 | The basic logic is as follows: 118 | 119 | * If struct has custom marshaller, call it and return 120 | * If struct has field "AdditionalFields", it must be a map[string]interface{} 121 | * If struct does not have "AdditionalFields", give an error 122 | * Get struct tag detailing json names to go names, create mapping 123 | * For each public field name 124 | - Write field name out 125 | - If field value is a struct, recursively call our state machine 126 | - Otherwise, use the json.Encoder to write out the value 127 | 128 | ## Unmarshalling 129 | 130 | The unmarshalling desin is also based around a statemachine design. The 131 | basic logic is as follows: 132 | 133 | * If struct has custom marhaller, call it 134 | * If struct has field "AdditionalFields", it must be a map[string]interface{} 135 | * Get struct tag detailing json names to go names, create mapping 136 | * For each key found 137 | - If key exists, 138 | - If value is basic type, extract value into struct field using Decoder 139 | - If value is struct type, recursively call statemachine 140 | - If key doesn't exist, add it to AdditionalFields if it exists using Decoder 141 | -------------------------------------------------------------------------------- /apps/internal/json/json.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | // Package json provide functions for marshalling an unmarshalling types to JSON. These functions are meant to 5 | // be utilized inside of structs that implement json.Unmarshaler and json.Marshaler interfaces. 6 | // This package provides the additional functionality of writing fields that are not in the struct when marshalling 7 | // to a field called AdditionalFields if that field exists and is a map[string]interface{}. 8 | // When marshalling, if the struct has all the same prerequisites, it will uses the keys in AdditionalFields as 9 | // extra fields. This package uses encoding/json underneath. 10 | package json 11 | 12 | import ( 13 | "bytes" 14 | "encoding/json" 15 | "fmt" 16 | "reflect" 17 | "strings" 18 | ) 19 | 20 | const addField = "AdditionalFields" 21 | 22 | var ( 23 | leftBrace = []byte("{")[0] 24 | rightBrace = []byte("}")[0] 25 | comma = []byte(",")[0] 26 | leftParen = []byte("[")[0] 27 | rightParen = []byte("]")[0] 28 | ) 29 | 30 | var mapStrInterType = reflect.TypeOf(map[string]interface{}{}) 31 | 32 | // stateFn defines a state machine function. This will be used in all state 33 | // machines in this package. 34 | type stateFn func() (stateFn, error) 35 | 36 | // Marshal is used to marshal a type into its JSON representation. It 37 | // wraps the stdlib calls in order to marshal a struct or *struct so 38 | // that a field called "AdditionalFields" of type map[string]interface{} 39 | // with "-" used inside struct tag `json:"-"` can be marshalled as if 40 | // they were fields within the struct. 41 | func Marshal(i interface{}) ([]byte, error) { 42 | buff := bytes.Buffer{} 43 | enc := json.NewEncoder(&buff) 44 | enc.SetEscapeHTML(false) 45 | enc.SetIndent("", "") 46 | 47 | v := reflect.ValueOf(i) 48 | if v.Kind() != reflect.Ptr && v.CanAddr() { 49 | v = v.Addr() 50 | } 51 | err := marshalStruct(v, &buff, enc) 52 | if err != nil { 53 | return nil, err 54 | } 55 | return buff.Bytes(), nil 56 | } 57 | 58 | // Unmarshal unmarshals a []byte representing JSON into i, which must be a *struct. In addition, if the struct has 59 | // a field called AdditionalFields of type map[string]interface{}, JSON data representing fields not in the struct 60 | // will be written as key/value pairs to AdditionalFields. 61 | func Unmarshal(b []byte, i interface{}) error { 62 | if len(b) == 0 { 63 | return nil 64 | } 65 | 66 | jdec := json.NewDecoder(bytes.NewBuffer(b)) 67 | jdec.UseNumber() 68 | return unmarshalStruct(jdec, i) 69 | } 70 | 71 | // MarshalRaw marshals i into a json.RawMessage. If I cannot be marshalled, 72 | // this will panic. This is exposed to help test AdditionalField values 73 | // which are stored as json.RawMessage. 74 | func MarshalRaw(i interface{}) json.RawMessage { 75 | b, err := json.Marshal(i) 76 | if err != nil { 77 | panic(err) 78 | } 79 | return json.RawMessage(b) 80 | } 81 | 82 | // isDelim simply tests to see if a json.Token is a delimeter. 83 | func isDelim(got json.Token) bool { 84 | switch got.(type) { 85 | case json.Delim: 86 | return true 87 | } 88 | return false 89 | } 90 | 91 | // delimIs tests got to see if it is want. 92 | func delimIs(got json.Token, want rune) bool { 93 | switch v := got.(type) { 94 | case json.Delim: 95 | if v == json.Delim(want) { 96 | return true 97 | } 98 | } 99 | return false 100 | } 101 | 102 | // hasMarshalJSON will determine if the value or a pointer to this value has 103 | // the MarshalJSON method. 104 | func hasMarshalJSON(v reflect.Value) bool { 105 | ok := false 106 | if _, ok = v.Interface().(json.Marshaler); !ok { 107 | var i any 108 | if v.Kind() == reflect.Ptr { 109 | i = v.Elem().Interface() 110 | } else if v.CanAddr() { 111 | i = v.Addr().Interface() 112 | } 113 | _, ok = i.(json.Marshaler) 114 | } 115 | return ok 116 | } 117 | 118 | // callMarshalJSON will call MarshalJSON() method on the value or a pointer to this value. 119 | // This will panic if the method is not defined. 120 | func callMarshalJSON(v reflect.Value) ([]byte, error) { 121 | if marsh, ok := v.Interface().(json.Marshaler); ok { 122 | return marsh.MarshalJSON() 123 | } 124 | 125 | if v.Kind() == reflect.Ptr { 126 | if marsh, ok := v.Elem().Interface().(json.Marshaler); ok { 127 | return marsh.MarshalJSON() 128 | } 129 | } else { 130 | if v.CanAddr() { 131 | if marsh, ok := v.Addr().Interface().(json.Marshaler); ok { 132 | return marsh.MarshalJSON() 133 | } 134 | } 135 | } 136 | 137 | panic(fmt.Sprintf("callMarshalJSON called on type %T that does not have MarshalJSON defined", v.Interface())) 138 | } 139 | 140 | // hasUnmarshalJSON will determine if the value or a pointer to this value has 141 | // the UnmarshalJSON method. 142 | func hasUnmarshalJSON(v reflect.Value) bool { 143 | // You can't unmarshal on a non-pointer type. 144 | if v.Kind() != reflect.Ptr { 145 | if !v.CanAddr() { 146 | return false 147 | } 148 | v = v.Addr() 149 | } 150 | 151 | _, ok := v.Interface().(json.Unmarshaler) 152 | return ok 153 | } 154 | 155 | // hasOmitEmpty indicates if the field has instructed us to not output 156 | // the field if omitempty is set on the tag. tag is the string 157 | // returned by reflect.StructField.Tag().Get(). 158 | func hasOmitEmpty(tag string) bool { 159 | sl := strings.Split(tag, ",") 160 | for _, str := range sl { 161 | if str == "omitempty" { 162 | return true 163 | } 164 | } 165 | return false 166 | } 167 | -------------------------------------------------------------------------------- /apps/internal/json/json_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | package json 5 | 6 | import ( 7 | "encoding/json" 8 | "fmt" 9 | "testing" 10 | "time" 11 | 12 | "github.com/kylelemons/godebug/pretty" 13 | ) 14 | 15 | type StructA struct { 16 | Name string 17 | ID int `json:"id"` 18 | Meta *StructB 19 | AdditionalFields map[string]interface{} 20 | } 21 | 22 | type StructB struct { 23 | Address string 24 | AdditionalFields map[string]interface{} 25 | } 26 | 27 | type StructC struct { 28 | Time time.Time 29 | Project StructD 30 | AdditionalFields map[string]interface{} 31 | } 32 | 33 | type StructD struct { 34 | Project string 35 | Info StructE 36 | AdditionalFields map[string]interface{} 37 | } 38 | 39 | type StructE struct { 40 | Employees int 41 | AdditionalFields map[string]interface{} 42 | } 43 | 44 | func TestUnmarshalRoundTrip(t *testing.T) { 45 | now := time.Now() 46 | nowJSON, err := now.MarshalJSON() 47 | if err != nil { 48 | panic(err) 49 | } 50 | 51 | tests := []struct { 52 | desc string 53 | b []byte 54 | got interface{} 55 | want interface{} 56 | err bool 57 | }{ 58 | { 59 | desc: "receiver not a pointer", 60 | got: StructA{}, 61 | b: []byte(`{"content": "value"}`), 62 | err: true, 63 | }, 64 | { 65 | desc: "receiver not a pointer to a struct", 66 | got: new(string), 67 | b: []byte(`{"content": "value"}`), 68 | err: true, 69 | }, 70 | { 71 | desc: "AdditionalFields not a map", 72 | b: []byte(`{"content": "value"}`), 73 | got: &struct { 74 | AdditionalFields string 75 | }{}, 76 | err: true, 77 | }, 78 | { 79 | desc: "Success, no json.Unmarshaler types", 80 | b: []byte( 81 | ` 82 | { 83 | "Name": "John", 84 | "id": 3, 85 | "Meta": { 86 | "Address": "291 Street", 87 | "unknown0": 3.2 88 | }, 89 | "unknown0": 10, 90 | "unknown1": "hello" 91 | } 92 | `, 93 | ), 94 | got: &StructA{}, 95 | want: &StructA{ 96 | Name: "John", 97 | ID: 3, 98 | Meta: &StructB{ 99 | Address: "291 Street", 100 | AdditionalFields: map[string]interface{}{ 101 | "unknown0": MarshalRaw(3.2), 102 | }, 103 | }, 104 | AdditionalFields: map[string]interface{}{ 105 | "unknown0": MarshalRaw(10), 106 | "unknown1": MarshalRaw("hello"), 107 | }, 108 | }, 109 | }, 110 | { 111 | desc: "Success, a type has json.Unmarshaler", 112 | b: []byte(fmt.Sprintf(` 113 | { 114 | "Time":%s, 115 | "Project": { 116 | "Project":"myProject", 117 | "Info":{ 118 | "Employees":2 119 | } 120 | } 121 | } 122 | `, string(nowJSON))), 123 | got: &StructC{}, 124 | want: &StructC{ 125 | Time: now, 126 | Project: StructD{ 127 | Project: "myProject", 128 | Info: StructE{ 129 | Employees: 2, 130 | }, 131 | }, 132 | }, 133 | }, 134 | } 135 | 136 | for _, test := range tests { 137 | err := Unmarshal(test.b, test.got) 138 | switch { 139 | case err == nil && test.err: 140 | t.Errorf("TestUnmarshal(%s): got err == nil, want err != nil", test.desc) 141 | continue 142 | case err != nil && !test.err: 143 | t.Errorf("TestUnmarshal(%s): got err == %s, want err == nil", test.desc, err) 144 | continue 145 | case err != nil: 146 | continue 147 | } 148 | if diff := (&pretty.Config{IncludeUnexported: false}).Compare(test.want, test.got); diff != "" { 149 | t.Errorf("TestUnmarshal(%s): -want/+got:\n%s", test.desc, diff) 150 | continue 151 | } 152 | b, err := Marshal(test.got) 153 | if err != nil { 154 | t.Errorf("TestUnmarshal(%s): Marshal failed: %s", test.desc, err) 155 | continue 156 | } 157 | err = Unmarshal(b, test.got) 158 | if err != nil { 159 | t.Errorf("TestUnmarshal(%s): Unmarshal round trip failed: %s", test.desc, err) 160 | continue 161 | } 162 | if diff := (&pretty.Config{IncludeUnexported: false}).Compare(test.want, test.got); diff != "" { 163 | t.Errorf("TestUnmarshal(%s): Round trip failed. -want/+got:\n%s", test.desc, diff) 164 | continue 165 | } 166 | } 167 | } 168 | 169 | func TestIsDelim(t *testing.T) { 170 | tests := []struct { 171 | desc string 172 | token json.Token 173 | want bool 174 | }{ 175 | {desc: "Is delim", token: json.Delim('{'), want: true}, 176 | {desc: "Not a delim", token: json.Token("{"), want: false}, 177 | } 178 | 179 | for _, test := range tests { 180 | got := isDelim(test.token) 181 | if got != test.want { 182 | t.Errorf("TestIsDelim(%s): got %v, want %v", test.desc, got, test.want) 183 | } 184 | } 185 | } 186 | 187 | func TestDelimIs(t *testing.T) { 188 | tests := []struct { 189 | desc string 190 | token json.Token 191 | delim rune 192 | want bool 193 | }{ 194 | {desc: "Token is a match", token: json.Delim('{'), delim: '{', want: true}, 195 | {desc: "Token is not a match", token: json.Delim('{'), delim: '}', want: false}, 196 | } 197 | 198 | for _, test := range tests { 199 | got := delimIs(test.token, test.delim) 200 | if got != test.want { 201 | t.Errorf("TestDelimIs(%s): got %v, want %v", test.desc, got, test.want) 202 | } 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /apps/internal/json/marshal_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | package json 5 | 6 | import ( 7 | "encoding/json" 8 | "testing" 9 | 10 | "github.com/kylelemons/godebug/pretty" 11 | ) 12 | 13 | func TestMarshalStruct(t *testing.T) { 14 | tests := []struct { 15 | desc string 16 | value interface{} 17 | want map[string]interface{} 18 | err bool 19 | }{ 20 | { 21 | desc: "struct with no additional fields", 22 | value: struct { 23 | Name string 24 | Int int 25 | }{ 26 | Name: "my name", 27 | Int: 5, 28 | }, 29 | want: map[string]interface{}{ 30 | "Name": "my name", 31 | "Int": 5, 32 | }, 33 | }, 34 | { 35 | desc: "*struct with AdditionalFields", 36 | value: &struct { 37 | Name string 38 | Int int 39 | AdditionalFields map[string]interface{} `json:"-"` 40 | }{ 41 | Name: "John Doak", 42 | Int: 45, 43 | AdditionalFields: map[string]interface{}{ 44 | "Hello": "World", 45 | "Float": 3.2, 46 | }, 47 | }, 48 | want: map[string]interface{}{ 49 | "Name": "John Doak", 50 | "Int": 45, 51 | "Float": 3.2, 52 | "Hello": "World", 53 | }, 54 | }, 55 | { 56 | desc: "AdditionalFields is not a map", 57 | value: struct { 58 | AdditionalFields string `json:"-"` 59 | }{ 60 | AdditionalFields: "hello", 61 | }, 62 | err: true, 63 | }, 64 | { 65 | desc: "AdditionalFields is not a map[string]interface{}", 66 | value: struct { 67 | AdditionalFields map[string]string `json:"-"` 68 | }{ 69 | AdditionalFields: map[string]string{ 70 | "Hello": "World", 71 | }, 72 | }, 73 | err: true, 74 | }, 75 | { 76 | desc: "Multiple Structs", 77 | value: &StructA{ 78 | Name: "John", 79 | ID: 3, 80 | Meta: &StructB{ 81 | Address: "291 Street", 82 | AdditionalFields: map[string]interface{}{ 83 | "unknown0": MarshalRaw(3.2), 84 | }, 85 | }, 86 | AdditionalFields: map[string]interface{}{ 87 | "unknown0": MarshalRaw(10), 88 | "unknown1": MarshalRaw("hello"), 89 | }, 90 | }, 91 | want: map[string]interface{}{ 92 | "Name": "John", 93 | "id": 3, 94 | "Meta": map[string]interface{}{ 95 | "Address": "291 Street", 96 | "unknown0": 3.2, 97 | }, 98 | "unknown0": 10, 99 | "unknown1": "hello", 100 | }, 101 | }, 102 | { 103 | desc: "Struct with map[string]interface{}", 104 | value: struct { 105 | Name string 106 | Map map[string]interface{} 107 | AdditionalFields map[string]interface{} 108 | }{ 109 | Name: "John", 110 | Map: map[string]interface{}{ 111 | "key": "value", 112 | }, 113 | }, 114 | want: map[string]interface{}{ 115 | "Name": "John", 116 | "Map": map[string]interface{}{ 117 | "key": "value", 118 | }, 119 | }, 120 | }, 121 | { 122 | desc: "Struct with map[string]struct{}", 123 | value: struct { 124 | Name string 125 | Map map[string]StructB 126 | AdditionalFields map[string]interface{} 127 | }{ 128 | Name: "John", 129 | Map: map[string]StructB{ 130 | "key": { 131 | Address: "addr", 132 | }, 133 | }, 134 | }, 135 | want: map[string]interface{}{ 136 | "Name": "John", 137 | "Map": map[string]interface{}{ 138 | "key": map[string]interface{}{ 139 | "Address": "addr", 140 | }, 141 | }, 142 | }, 143 | }, 144 | { 145 | desc: "Struct with map[string][]", 146 | value: struct { 147 | Name string 148 | Map map[string]interface{} 149 | AdditionalFields map[string]interface{} 150 | }{ 151 | Name: "John", 152 | Map: map[string]interface{}{ 153 | "key": []string{ 154 | "apples", 155 | }, 156 | }, 157 | }, 158 | want: map[string]interface{}{ 159 | "Name": "John", 160 | "Map": map[string]interface{}{ 161 | "key": []string{"apples"}, 162 | }, 163 | }, 164 | }, 165 | { 166 | desc: "Struct with map[string][]struct", 167 | value: struct { 168 | Name string 169 | Map map[string][]StructB 170 | AdditionalFields map[string]interface{} 171 | }{ 172 | Name: "John", 173 | Map: map[string][]StructB{ 174 | "key": { 175 | {Address: "addr"}, 176 | }, 177 | }, 178 | }, 179 | want: map[string]interface{}{ 180 | "Name": "John", 181 | "Map": map[string]interface{}{ 182 | "key": []interface{}{ 183 | map[string]interface{}{ 184 | "Address": "addr", 185 | }, 186 | }, 187 | }, 188 | }, 189 | }, 190 | } 191 | 192 | for _, test := range tests { 193 | b, err := Marshal(test.value) 194 | switch { 195 | case err == nil && test.err: 196 | t.Errorf("TestMarshal(%s): got err == nil, want err != nil", test.desc) 197 | continue 198 | case err != nil && !test.err: 199 | t.Errorf("TestMarshal(%s): got err == %s, want err == nil", test.desc, err) 200 | continue 201 | case err != nil: 202 | continue 203 | } 204 | 205 | got := map[string]interface{}{} 206 | if err := json.Unmarshal(b, &got); err != nil { 207 | t.Errorf("TestMarshal(%s): Marshal produced invalid JSON:\n%s\n%s", test.desc, err, string(b)) 208 | continue 209 | } 210 | if diff := pretty.Compare(test.want, got); diff != "" { 211 | t.Errorf("TestMarshal(%s): -want/+got:\n%s", test.desc, diff) 212 | } 213 | } 214 | } 215 | 216 | func TestEmptyTypes(t *testing.T) { 217 | type structA struct { 218 | EmptyMap map[string]bool 219 | EmptySlice []string 220 | Slice []string 221 | EmptyInt int 222 | Int int 223 | 224 | AdditionalFields map[string]interface{} 225 | } 226 | 227 | val := structA{ 228 | EmptyMap: map[string]bool{}, 229 | Slice: []string{"hello"}, 230 | Int: 1, 231 | } 232 | 233 | b, err := Marshal(val) 234 | if err != nil { 235 | t.Fatalf("TestEmptyTypes: unexpected error on Marshal: %v", err) 236 | } 237 | 238 | got := structA{} 239 | 240 | if err := Unmarshal(b, &got); err != nil { 241 | t.Fatalf("TestEmptyTypes: unexpected error when Umarshalling: %v", err) 242 | } 243 | 244 | if diff := pretty.Compare(got, val); diff != "" { 245 | t.Fatalf("TestEmptyTypes: -want/+got:\n%s", diff) 246 | } 247 | } 248 | -------------------------------------------------------------------------------- /apps/internal/json/struct_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | package json 5 | 6 | import ( 7 | "bytes" 8 | "encoding/json" 9 | "reflect" 10 | "runtime" 11 | "testing" 12 | 13 | "github.com/kylelemons/godebug/pretty" 14 | ) 15 | 16 | func TestDecoderStart(t *testing.T) { 17 | tests := []struct { 18 | desc string 19 | b []byte 20 | i interface{} 21 | stateFn stateFn 22 | err bool 23 | }{ 24 | { 25 | desc: "No content to decode", 26 | i: &StructA{}, 27 | stateFn: nil, 28 | err: true, 29 | }, 30 | { 31 | desc: "No opening brace", 32 | b: []byte("3"), 33 | i: &StructA{}, 34 | stateFn: nil, 35 | err: true, 36 | }, 37 | { 38 | desc: "Success", 39 | b: []byte(`{"Name": "value"}`), 40 | i: &StructA{}, 41 | stateFn: (new(decoder).next), 42 | }, 43 | } 44 | 45 | for _, test := range tests { 46 | dec := newDecoder(json.NewDecoder(bytes.NewBuffer(test.b)), reflect.ValueOf(test.i)) 47 | stateFn, err := dec.start() 48 | switch { 49 | case err == nil && test.err: 50 | t.Errorf("TestDecoderStart(%s): got err == nil, want err != nil", test.desc) 51 | continue 52 | case err != nil && !test.err: 53 | t.Errorf("TestDecoderStart(%s): got err == %s, want err == nil", test.desc, err) 54 | continue 55 | case err != nil: 56 | continue 57 | } 58 | 59 | gotStateFn := runtime.FuncForPC(reflect.ValueOf(stateFn).Pointer()).Name() 60 | wantStateFn := runtime.FuncForPC(reflect.ValueOf(test.stateFn).Pointer()).Name() 61 | if gotStateFn != wantStateFn { 62 | t.Errorf("TestDecoderStart(%s): got(stateFn) %s, want %s", test.desc, gotStateFn, wantStateFn) 63 | } 64 | } 65 | } 66 | 67 | func TestDecoderNext(t *testing.T) { 68 | tests := []struct { 69 | desc string 70 | b []byte 71 | // advToken advanced the decoder this may Token() calls, as the decoder only works 72 | // on well formed JSON. 73 | advToken int 74 | i interface{} 75 | key string 76 | stateFn stateFn 77 | err bool 78 | }{ 79 | { 80 | desc: "No content to decode", 81 | i: &StructA{}, 82 | stateFn: nil, 83 | err: true, 84 | }, 85 | { 86 | desc: "Bad ] found", 87 | b: []byte("{]"), 88 | advToken: 1, 89 | i: &StructA{}, 90 | stateFn: nil, 91 | err: true, 92 | }, 93 | { 94 | desc: "Closing brace", 95 | b: []byte("{}"), 96 | advToken: 1, 97 | i: &StructA{}, 98 | stateFn: nil, 99 | err: false, 100 | }, 101 | { 102 | desc: "Success", 103 | b: []byte(`{"Name": "value"}`), 104 | advToken: 1, 105 | i: &StructA{}, 106 | key: "Name", 107 | stateFn: (new(decoder).storeValue), 108 | }, 109 | } 110 | 111 | for _, test := range tests { 112 | dec := newDecoder(json.NewDecoder(bytes.NewBuffer(test.b)), reflect.ValueOf(test.i)) 113 | for i := 0; i < test.advToken; i++ { 114 | if _, err := dec.dec.Token(); err != nil { 115 | panic(err) 116 | } 117 | } 118 | 119 | stateFn, err := dec.next() 120 | switch { 121 | case err == nil && test.err: 122 | t.Errorf("TestDecoderNext(%s): got err == nil, want err != nil", test.desc) 123 | continue 124 | case err != nil && !test.err: 125 | t.Errorf("TestDecoderNext(%s): got err == %s, want err == nil", test.desc, err) 126 | continue 127 | case err != nil: 128 | continue 129 | } 130 | 131 | if dec.key != test.key { 132 | t.Errorf("TestDecoderNext(%s): got(.key) %s, want %s", test.desc, dec.key, test.key) 133 | } 134 | 135 | gotStateFn := runtime.FuncForPC(reflect.ValueOf(stateFn).Pointer()).Name() 136 | wantStateFn := runtime.FuncForPC(reflect.ValueOf(test.stateFn).Pointer()).Name() 137 | if gotStateFn != wantStateFn { 138 | t.Errorf("TestDecoderNext(%s): got(stateFn) %s, want %s", test.desc, gotStateFn, wantStateFn) 139 | } 140 | } 141 | } 142 | 143 | func TestDecoderStoreValue(t *testing.T) { 144 | tests := []struct { 145 | desc string 146 | b []byte 147 | want StructA 148 | stateFn stateFn 149 | }{ 150 | { 151 | desc: "Field found, no struct tag", 152 | b: []byte(`{"Name": "myName"}`), 153 | want: StructA{Name: "myName"}, 154 | stateFn: (new(decoder).next), 155 | }, 156 | { 157 | desc: "Field found, using struct tag", 158 | b: []byte(`{"id": 3}`), 159 | want: StructA{ID: 3}, 160 | stateFn: (new(decoder).next), 161 | }, 162 | { 163 | desc: "Field not found, go to storeAdditional()", 164 | b: []byte(`{"blah": 3}`), 165 | want: StructA{}, 166 | stateFn: (new(decoder).storeAdditional), 167 | }, 168 | } 169 | 170 | for _, test := range tests { 171 | got := StructA{} 172 | dec := newDecoder(json.NewDecoder(bytes.NewBuffer(test.b)), reflect.ValueOf(&got).Elem()) 173 | _, err := dec.start() // populates our translator field 174 | if err != nil { 175 | panic(err) 176 | } 177 | _, err = dec.next() 178 | if err != nil { 179 | panic(err) 180 | } 181 | 182 | stateFn, err := dec.storeValue() 183 | if err != nil { 184 | t.Errorf("TestDecoderStoreValue(%s): got err == %s, want err == nil", test.desc, err) 185 | continue 186 | } 187 | 188 | if diff := pretty.Compare(test.want, got); diff != "" { 189 | t.Errorf("TestDecoderStoreValue(%s): -want/+got:\n%s", test.desc, diff) 190 | continue 191 | } 192 | 193 | gotStateFn := runtime.FuncForPC(reflect.ValueOf(stateFn).Pointer()).Name() 194 | wantStateFn := runtime.FuncForPC(reflect.ValueOf(test.stateFn).Pointer()).Name() 195 | if gotStateFn != wantStateFn { 196 | t.Errorf("TestDecoderStoreValue(%s): got(stateFn) %s, want %s", test.desc, gotStateFn, wantStateFn) 197 | } 198 | } 199 | } 200 | 201 | func TestDecoderStoreAdditional(t *testing.T) { 202 | tests := []struct { 203 | desc string 204 | b []byte 205 | got StructA 206 | want StructA 207 | stateFn stateFn 208 | }{ 209 | { 210 | desc: "Map not initialized", 211 | b: []byte(`{"blah": "whatever"}`), 212 | got: StructA{}, 213 | want: StructA{ 214 | AdditionalFields: map[string]interface{}{ 215 | "blah": json.RawMessage(`"whatever"`), 216 | }, 217 | }, 218 | stateFn: (new(decoder).next), 219 | }, 220 | { 221 | desc: "Map exists", 222 | b: []byte(`{"blah": "whatever"}`), 223 | got: StructA{ 224 | AdditionalFields: map[string]interface{}{ 225 | "else": json.RawMessage(`"if"`), 226 | }, 227 | }, 228 | want: StructA{ 229 | AdditionalFields: map[string]interface{}{ 230 | "else": json.RawMessage(`"if"`), 231 | "blah": json.RawMessage(`"whatever"`), 232 | }, 233 | }, 234 | stateFn: (new(decoder).next), 235 | }, 236 | } 237 | 238 | for _, test := range tests { 239 | dec := newDecoder(json.NewDecoder(bytes.NewBuffer(test.b)), reflect.ValueOf(&test.got).Elem()) 240 | _, err := dec.start() // populates our translator field 241 | if err != nil { 242 | panic(err) 243 | } 244 | _, err = dec.next() 245 | if err != nil { 246 | panic(err) 247 | } 248 | 249 | stateFn, err := dec.storeAdditional() 250 | if err != nil { 251 | t.Errorf("TestDecoderStoreAdditional(%s): got err == %s, want err == nil", test.desc, err) 252 | continue 253 | } 254 | 255 | if diff := pretty.Compare(test.want, test.got); diff != "" { 256 | t.Errorf("TestDecoderStoreAdditional(%s): -want/+got:\n%s", test.desc, diff) 257 | continue 258 | } 259 | 260 | gotStateFn := runtime.FuncForPC(reflect.ValueOf(stateFn).Pointer()).Name() 261 | wantStateFn := runtime.FuncForPC(reflect.ValueOf(test.stateFn).Pointer()).Name() 262 | if gotStateFn != wantStateFn { 263 | t.Errorf("TestDecoderStoreAdditional(%s): got(stateFn) %s, want %s", test.desc, gotStateFn, wantStateFn) 264 | } 265 | } 266 | } 267 | -------------------------------------------------------------------------------- /apps/internal/json/types/time/time.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | // Package time provides for custom types to translate time from JSON and other formats 5 | // into time.Time objects. 6 | package time 7 | 8 | import ( 9 | "fmt" 10 | "strconv" 11 | "strings" 12 | "time" 13 | ) 14 | 15 | // Unix provides a type that can marshal and unmarshal a string representation 16 | // of the unix epoch into a time.Time object. 17 | type Unix struct { 18 | T time.Time 19 | } 20 | 21 | // MarshalJSON implements encoding/json.MarshalJSON(). 22 | func (u Unix) MarshalJSON() ([]byte, error) { 23 | if u.T.IsZero() { 24 | return []byte(""), nil 25 | } 26 | return []byte(fmt.Sprintf("%q", strconv.FormatInt(u.T.Unix(), 10))), nil 27 | } 28 | 29 | // UnmarshalJSON implements encoding/json.UnmarshalJSON(). 30 | func (u *Unix) UnmarshalJSON(b []byte) error { 31 | i, err := strconv.Atoi(strings.Trim(string(b), `"`)) 32 | if err != nil { 33 | return fmt.Errorf("unix time(%s) could not be converted from string to int: %w", string(b), err) 34 | } 35 | u.T = time.Unix(int64(i), 0) 36 | return nil 37 | } 38 | 39 | // DurationTime provides a type that can marshal and unmarshal a string representation 40 | // of a duration from now into a time.Time object. 41 | // Note: I'm not sure this is the best way to do this. What happens is we get a field 42 | // called "expires_in" that represents the seconds from now that this expires. We 43 | // turn that into a time we call .ExpiresOn. But maybe we should be recording 44 | // when the token was received at .TokenRecieved and .ExpiresIn should remain as a duration. 45 | // Then we could have a method called ExpiresOn(). Honestly, the whole thing is 46 | // bad because the server doesn't return a concrete time. I think this is 47 | // cleaner, but its not great either. 48 | type DurationTime struct { 49 | T time.Time 50 | } 51 | 52 | // MarshalJSON implements encoding/json.MarshalJSON(). 53 | func (d DurationTime) MarshalJSON() ([]byte, error) { 54 | if d.T.IsZero() { 55 | return []byte(""), nil 56 | } 57 | 58 | dt := time.Until(d.T) 59 | return []byte(fmt.Sprintf("%d", int64(dt*time.Second))), nil 60 | } 61 | 62 | // UnmarshalJSON implements encoding/json.UnmarshalJSON(). 63 | func (d *DurationTime) UnmarshalJSON(b []byte) error { 64 | i, err := strconv.Atoi(strings.Trim(string(b), `"`)) 65 | if err != nil { 66 | return fmt.Errorf("unix time(%s) could not be converted from string to int: %w", string(b), err) 67 | } 68 | d.T = time.Now().Add(time.Duration(i) * time.Second) 69 | return nil 70 | } 71 | -------------------------------------------------------------------------------- /apps/internal/local/server.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | // Package local contains a local HTTP server used with interactive authentication. 5 | package local 6 | 7 | import ( 8 | "context" 9 | "fmt" 10 | "html" 11 | "net" 12 | "net/http" 13 | "strconv" 14 | "strings" 15 | "time" 16 | ) 17 | 18 | var okPage = []byte(` 19 | 20 | 21 | 22 | 23 | Authentication Complete 24 | 25 | 26 |

Authentication complete. You can return to the application. Feel free to close this browser tab.

27 | 28 | 29 | `) 30 | 31 | const failPage = ` 32 | 33 | 34 | 35 | 36 | Authentication Failed 37 | 38 | 39 |

Authentication failed. You can return to the application. Feel free to close this browser tab.

40 |

Error details: error %s error_description: %s

41 | 42 | 43 | ` 44 | 45 | // Result is the result from the redirect. 46 | type Result struct { 47 | // Code is the code sent by the authority server. 48 | Code string 49 | // Err is set if there was an error. 50 | Err error 51 | } 52 | 53 | // Server is an HTTP server. 54 | type Server struct { 55 | // Addr is the address the server is listening on. 56 | Addr string 57 | resultCh chan Result 58 | s *http.Server 59 | reqState string 60 | } 61 | 62 | // New creates a local HTTP server and starts it. 63 | func New(reqState string, port int) (*Server, error) { 64 | var l net.Listener 65 | var err error 66 | var portStr string 67 | if port > 0 { 68 | // use port provided by caller 69 | l, err = net.Listen("tcp", fmt.Sprintf("localhost:%d", port)) 70 | portStr = strconv.FormatInt(int64(port), 10) 71 | } else { 72 | // find a free port 73 | for i := 0; i < 10; i++ { 74 | l, err = net.Listen("tcp", "localhost:0") 75 | if err != nil { 76 | continue 77 | } 78 | addr := l.Addr().String() 79 | portStr = addr[strings.LastIndex(addr, ":")+1:] 80 | break 81 | } 82 | } 83 | if err != nil { 84 | return nil, err 85 | } 86 | 87 | serv := &Server{ 88 | Addr: fmt.Sprintf("http://localhost:%s", portStr), 89 | s: &http.Server{Addr: "localhost:0", ReadHeaderTimeout: time.Second}, 90 | reqState: reqState, 91 | resultCh: make(chan Result, 1), 92 | } 93 | serv.s.Handler = http.HandlerFunc(serv.handler) 94 | 95 | if err := serv.start(l); err != nil { 96 | return nil, err 97 | } 98 | 99 | return serv, nil 100 | } 101 | 102 | func (s *Server) start(l net.Listener) error { 103 | go func() { 104 | err := s.s.Serve(l) 105 | if err != nil { 106 | select { 107 | case s.resultCh <- Result{Err: err}: 108 | default: 109 | } 110 | } 111 | }() 112 | 113 | return nil 114 | } 115 | 116 | // Result gets the result of the redirect operation. Once a single result is returned, the server 117 | // is shutdown. ctx deadline will be honored. 118 | func (s *Server) Result(ctx context.Context) Result { 119 | select { 120 | case <-ctx.Done(): 121 | return Result{Err: ctx.Err()} 122 | case r := <-s.resultCh: 123 | return r 124 | } 125 | } 126 | 127 | // Shutdown shuts down the server. 128 | func (s *Server) Shutdown() { 129 | // Note: You might get clever and think you can do this in handler() as a defer, you can't. 130 | _ = s.s.Shutdown(context.Background()) 131 | } 132 | 133 | func (s *Server) putResult(r Result) { 134 | select { 135 | case s.resultCh <- r: 136 | default: 137 | } 138 | } 139 | 140 | func (s *Server) handler(w http.ResponseWriter, r *http.Request) { 141 | q := r.URL.Query() 142 | 143 | headerErr := q.Get("error") 144 | if headerErr != "" { 145 | desc := html.EscapeString(q.Get("error_description")) 146 | escapedHeaderErr := html.EscapeString(headerErr) 147 | // Note: It is a little weird we handle some errors by not going to the failPage. If they all should, 148 | // change this to s.error() and make s.error() write the failPage instead of an error code. 149 | _, _ = w.Write([]byte(fmt.Sprintf(failPage, escapedHeaderErr, desc))) 150 | s.putResult(Result{Err: fmt.Errorf("%s", desc)}) 151 | 152 | return 153 | } 154 | 155 | respState := q.Get("state") 156 | switch respState { 157 | case s.reqState: 158 | case "": 159 | s.error(w, http.StatusInternalServerError, "server didn't send OAuth state") 160 | return 161 | default: 162 | s.error(w, http.StatusInternalServerError, "mismatched OAuth state, req(%s), resp(%s)", s.reqState, respState) 163 | return 164 | } 165 | 166 | code := q.Get("code") 167 | if code == "" { 168 | s.error(w, http.StatusInternalServerError, "authorization code missing in query string") 169 | return 170 | } 171 | 172 | _, _ = w.Write(okPage) 173 | s.putResult(Result{Code: code}) 174 | } 175 | 176 | func (s *Server) error(w http.ResponseWriter, code int, str string, i ...interface{}) { 177 | err := fmt.Errorf(str, i...) 178 | http.Error(w, err.Error(), code) 179 | s.putResult(Result{Err: err}) 180 | } 181 | -------------------------------------------------------------------------------- /apps/internal/local/server_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | package local 5 | 6 | import ( 7 | "context" 8 | "io" 9 | "net/http" 10 | "net/url" 11 | "strings" 12 | "testing" 13 | "time" 14 | 15 | "github.com/kylelemons/godebug/pretty" 16 | ) 17 | 18 | func TestServer(t *testing.T) { 19 | ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 20 | defer cancel() 21 | 22 | tests := []struct { 23 | desc string 24 | reqState string 25 | port int 26 | q url.Values 27 | failPage bool 28 | statusCode int 29 | }{ 30 | { 31 | desc: "Error: Query Values has 'error' key", 32 | reqState: "state", 33 | port: 0, 34 | q: url.Values{"state": []string{"state"}, "error": []string{"error"}}, 35 | statusCode: 200, 36 | failPage: true, 37 | }, 38 | { 39 | desc: "Error: Query Values missing 'state' key", 40 | reqState: "state", 41 | port: 0, 42 | q: url.Values{"code": []string{"code"}}, 43 | statusCode: http.StatusInternalServerError, 44 | }, 45 | { 46 | desc: "Error: Query Values missing had 'state' key value that was different that requested", 47 | reqState: "state", 48 | port: 0, 49 | q: url.Values{"state": []string{"etats"}, "code": []string{"code"}}, 50 | statusCode: http.StatusInternalServerError, 51 | }, 52 | { 53 | desc: "Error: Query Values missing 'code' key", 54 | reqState: "state", 55 | port: 0, 56 | q: url.Values{"state": []string{"state"}}, 57 | statusCode: http.StatusInternalServerError, 58 | }, 59 | { 60 | desc: "Success", 61 | reqState: "state", 62 | port: 0, 63 | q: url.Values{"state": []string{"state"}, "code": []string{"code"}}, 64 | statusCode: 200, 65 | }, 66 | } 67 | 68 | for _, test := range tests { 69 | serv, err := New(test.reqState, test.port) 70 | if err != nil { 71 | panic(err) 72 | } 73 | defer serv.Shutdown() 74 | 75 | if !strings.HasPrefix(serv.Addr, "http://localhost") { 76 | t.Fatalf("unexpected server address %s", serv.Addr) 77 | } 78 | u, err := url.Parse(serv.Addr) 79 | if err != nil { 80 | panic(err) 81 | } 82 | u.RawQuery = test.q.Encode() 83 | 84 | resp, err := http.DefaultClient.Do( 85 | &http.Request{ 86 | Method: "GET", 87 | URL: u, 88 | }, 89 | ) 90 | 91 | if err != nil { 92 | panic(err) 93 | } 94 | 95 | if resp.StatusCode != test.statusCode { 96 | if test.statusCode == 200 { 97 | t.Errorf("TestServer(%s): got StatusCode == %d, want StatusCode == 200", test.desc, resp.StatusCode) 98 | res := serv.Result(ctx) 99 | if res.Err == nil { 100 | t.Errorf("TestServer(%s): Result.Err == nil, want Result.Err != nil", test.desc) 101 | } 102 | continue 103 | } 104 | t.Errorf("TestServer(%s): got StatusCode == %d, want StatusCode == %d", test.desc, resp.StatusCode, test.statusCode) 105 | res := serv.Result(ctx) 106 | if res.Err == nil { 107 | t.Errorf("TestServer(%s): Result.Err == nil, want Result.Err != nil", test.desc) 108 | } 109 | continue 110 | } 111 | if resp.StatusCode != 200 { 112 | continue 113 | } 114 | 115 | content, err := io.ReadAll(resp.Body) 116 | if err != nil { 117 | panic(err) 118 | } 119 | 120 | if test.failPage { 121 | if !strings.Contains(string(content), "Authentication Failed") { 122 | t.Errorf("TestServer(%s): got okay page, want failed page", test.desc) 123 | } 124 | 125 | res := serv.Result(ctx) 126 | if res.Err == nil { 127 | t.Errorf("TestServer(%s): Result.Err == nil, want Result.Err != nil", test.desc) 128 | } 129 | continue 130 | } 131 | 132 | if !strings.Contains(string(content), "Authentication Complete") { 133 | t.Errorf("TestServer(%s): got failed page, okay page", test.desc) 134 | } 135 | 136 | res := serv.Result(ctx) 137 | if diff := pretty.Compare(Result{Code: "code"}, res); diff != "" { 138 | t.Errorf("TestServer(%s): -want/+got:\n%s", test.desc, diff) 139 | } 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /apps/internal/mock/mock.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | package mock 5 | 6 | import ( 7 | "bytes" 8 | "encoding/base64" 9 | "fmt" 10 | "io" 11 | "net/http" 12 | "strings" 13 | "sync" 14 | "time" 15 | 16 | "github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/oauth/ops/authority" 17 | ) 18 | 19 | type response struct { 20 | body []byte 21 | callback func(*http.Request) 22 | code int 23 | headers http.Header 24 | } 25 | 26 | type responseOption interface { 27 | apply(*response) 28 | } 29 | 30 | type respOpt func(*response) 31 | 32 | func (fn respOpt) apply(r *response) { 33 | fn(r) 34 | } 35 | 36 | // WithBody sets the HTTP response's body to the specified value. 37 | func WithBody(b []byte) responseOption { 38 | return respOpt(func(r *response) { 39 | r.body = b 40 | }) 41 | } 42 | 43 | // WithCallback sets a callback to invoke before returning the response. 44 | func WithCallback(callback func(*http.Request)) responseOption { 45 | return respOpt(func(r *response) { 46 | r.callback = callback 47 | }) 48 | } 49 | 50 | // WithHTTPHeader sets the HTTP headers of the response to the specified value. 51 | func WithHTTPHeader(header http.Header) responseOption { 52 | return respOpt(func(r *response) { 53 | r.headers = header 54 | }) 55 | } 56 | 57 | // WithHTTPStatusCode sets the HTTP statusCode of response to the specified value. 58 | func WithHTTPStatusCode(statusCode int) responseOption { 59 | return respOpt(func(r *response) { 60 | r.code = statusCode 61 | }) 62 | } 63 | 64 | // Client is a mock HTTP client that returns a sequence of responses. Use AppendResponse to specify the sequence. 65 | type Client struct { 66 | mu *sync.Mutex 67 | resp []response 68 | } 69 | 70 | func NewClient() *Client { 71 | return &Client{mu: &sync.Mutex{}} 72 | } 73 | 74 | func (c *Client) AppendResponse(opts ...responseOption) { 75 | c.mu.Lock() 76 | defer c.mu.Unlock() 77 | r := response{code: http.StatusOK, headers: http.Header{}} 78 | for _, o := range opts { 79 | o.apply(&r) 80 | } 81 | c.resp = append(c.resp, r) 82 | } 83 | 84 | func (c *Client) Do(req *http.Request) (*http.Response, error) { 85 | c.mu.Lock() 86 | defer c.mu.Unlock() 87 | if len(c.resp) == 0 { 88 | panic(fmt.Sprintf(`no response for "%s"`, req.URL.String())) 89 | } 90 | resp := c.resp[0] 91 | c.resp = c.resp[1:] 92 | if resp.callback != nil { 93 | resp.callback(req) 94 | } 95 | res := http.Response{Header: resp.headers, StatusCode: resp.code} 96 | res.Body = io.NopCloser(bytes.NewReader(resp.body)) 97 | return &res, nil 98 | } 99 | 100 | // CloseIdleConnections implements the comm.HTTPClient interface 101 | func (*Client) CloseIdleConnections() {} 102 | 103 | func GetAccessTokenBody(accessToken, idToken, refreshToken, clientInfo string, expiresIn, refreshIn int) []byte { 104 | // Start building the body with the common fields 105 | body := fmt.Sprintf( 106 | `{"access_token": "%s","expires_in": %d,"expires_on": %d,"token_type": "Bearer"`, 107 | accessToken, expiresIn, time.Now().Add(time.Duration(expiresIn)*time.Second).Unix(), 108 | ) 109 | 110 | // Conditionally add the "refresh_in" field if refreshIn is provided 111 | if refreshIn > 0 { 112 | body += fmt.Sprintf(`, "refresh_in":"%d"`, refreshIn) 113 | } 114 | 115 | // Add the optional fields if they are provided 116 | if clientInfo != "" { 117 | body += fmt.Sprintf(`, "client_info": "%s"`, clientInfo) 118 | } 119 | if idToken != "" { 120 | body += fmt.Sprintf(`, "id_token": "%s"`, idToken) 121 | } 122 | if refreshToken != "" { 123 | body += fmt.Sprintf(`, "refresh_token": "%s"`, refreshToken) 124 | } 125 | 126 | // Close the JSON string 127 | body += "}" 128 | 129 | return []byte(body) 130 | } 131 | 132 | func GetIDToken(tenant, issuer string) string { 133 | now := time.Now().Unix() 134 | payload := []byte(fmt.Sprintf(`{"aud": "%s","exp": %d,"iat": %d,"iss": "%s","tid": "%s"}`, tenant, now+3600, now, issuer, tenant)) 135 | return fmt.Sprintf("header.%s.signature", base64.RawStdEncoding.EncodeToString(payload)) 136 | } 137 | 138 | func GetInstanceDiscoveryBody(host, tenant string) []byte { 139 | authority := fmt.Sprintf("https://%s/%s", host, tenant) 140 | body := fmt.Sprintf(`{"tenant_discovery_endpoint": "%s/v2.0/.well-known/openid-configuration","api-version": "1.1","metadata": [{"preferred_network": "%s","preferred_cache": "%s","aliases": ["%s"]}]}`, 141 | authority, host, host, host, 142 | ) 143 | headers := http.Header{} 144 | headers.Add("Content-Type", "application/json; charset=utf-8") 145 | return []byte(body) 146 | } 147 | 148 | func GetTenantDiscoveryBody(host, tenant string) []byte { 149 | authority := fmt.Sprintf("https://%s/%s", host, tenant) 150 | content := strings.ReplaceAll(`{"token_endpoint": "{authority}/oauth2/v2.0/token", 151 | "token_endpoint_auth_methods_supported": [ 152 | "client_secret_post", 153 | "private_key_jwt", 154 | "client_secret_basic" 155 | ], 156 | "jwks_uri": "{authority}/discovery/v2.0/keys", 157 | "response_modes_supported": [ 158 | "query", 159 | "fragment", 160 | "form_post" 161 | ], 162 | "subject_types_supported": [ 163 | "pairwise" 164 | ], 165 | "id_token_signing_alg_values_supported": [ 166 | "RS256" 167 | ], 168 | "response_types_supported": [ 169 | "code", 170 | "id_token", 171 | "code id_token", 172 | "id_token token" 173 | ], 174 | "scopes_supported": [ 175 | "openid", 176 | "profile", 177 | "email", 178 | "offline_access" 179 | ], 180 | "issuer": "{authority}/v2.0", 181 | "request_uri_parameter_supported": false, 182 | "userinfo_endpoint": "https://graph.microsoft.com/oidc/userinfo", 183 | "authorization_endpoint": "{authority}/oauth2/v2.0/authorize", 184 | "device_authorization_endpoint": "{authority}/oauth2/v2.0/devicecode", 185 | "http_logout_supported": true, 186 | "frontchannel_logout_supported": true, 187 | "end_session_endpoint": "{authority}/oauth2/v2.0/logout", 188 | "claims_supported": [ 189 | "sub", 190 | "iss", 191 | "cloud_instance_name", 192 | "cloud_instance_host_name", 193 | "cloud_graph_host_name", 194 | "msgraph_host", 195 | "aud", 196 | "exp", 197 | "iat", 198 | "auth_time", 199 | "acr", 200 | "nonce", 201 | "preferred_username", 202 | "name", 203 | "tid", 204 | "ver", 205 | "at_hash", 206 | "c_hash", 207 | "email" 208 | ], 209 | "kerberos_endpoint": "{authority}/kerberos", 210 | "tenant_region_scope": "NA", 211 | "cloud_instance_name": "microsoftonline.com", 212 | "cloud_graph_host_name": "graph.windows.net", 213 | "msgraph_host": "graph.microsoft.com", 214 | "rbac_url": "https://pas.windows.net" 215 | }`, "{authority}", authority) 216 | return []byte(content) 217 | } 218 | 219 | const Authnschemeformat = "%s-formated" 220 | 221 | type AuthnSchemeTest struct { 222 | } 223 | 224 | func (a *AuthnSchemeTest) TokenRequestParams() map[string]string { 225 | return map[string]string{ 226 | "foo": "bar", 227 | "customHeader": "customHeaderValue", 228 | } 229 | } 230 | 231 | func (a *AuthnSchemeTest) KeyID() string { 232 | return "KeyId" 233 | } 234 | 235 | func (a *AuthnSchemeTest) FormatAccessToken(accessToken string) (string, error) { 236 | return fmt.Sprintf(Authnschemeformat, accessToken), nil 237 | } 238 | 239 | func (a *AuthnSchemeTest) AccessTokenType() string { 240 | return "TokenType" 241 | } 242 | 243 | func NewTestAuthnScheme() authority.AuthenticationScheme { 244 | return &AuthnSchemeTest{} 245 | } 246 | -------------------------------------------------------------------------------- /apps/internal/oauth/fake/fake.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | package fake 5 | 6 | import ( 7 | "context" 8 | "errors" 9 | "fmt" 10 | 11 | "github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/oauth/ops/accesstokens" 12 | "github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/oauth/ops/authority" 13 | "github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/oauth/ops/wstrust" 14 | "github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/oauth/ops/wstrust/defs" 15 | ) 16 | 17 | // ResolveEndpoints is a fake implementation of the oauth.resolveEndpointer interface. 18 | type ResolveEndpoints struct { 19 | // Set this to true to have all APIs return an error. 20 | Err bool 21 | 22 | // fake result to return 23 | Endpoints authority.Endpoints 24 | } 25 | 26 | func (f ResolveEndpoints) ResolveEndpoints(ctx context.Context, authorityInfo authority.Info, userPrincipalName string) (authority.Endpoints, error) { 27 | if f.Err { 28 | return authority.Endpoints{}, errors.New("error") 29 | } 30 | return f.Endpoints, nil 31 | } 32 | 33 | // AccessTokens is a fake implementation of the oauth.accessTokens interface. 34 | type AccessTokens struct { 35 | // Set this to true to have all APIs return an error. 36 | Err bool 37 | 38 | // Result is for use with FromDeviceCodeResult. On each call it returns 39 | // the next item in this slice. They must be either an error or nil. 40 | Result []error 41 | Next int 42 | 43 | // fake result to return 44 | AccessToken accesstokens.TokenResponse 45 | 46 | // fake result to return 47 | DeviceCode accesstokens.DeviceCodeResult 48 | 49 | // FromRefreshTokenCallback is an optional callback invoked by FromRefreshToken 50 | FromRefreshTokenCallback func(appType accesstokens.AppType, authParams authority.AuthParams, cc *accesstokens.Credential, refreshToken string) 51 | 52 | // ValidateAssertion is an optional callback for validating an assertion generated by confidential.Client 53 | ValidateAssertion func(string) 54 | } 55 | 56 | func (f *AccessTokens) FromUsernamePassword(ctx context.Context, authParameters authority.AuthParams) (accesstokens.TokenResponse, error) { 57 | if f.Err { 58 | return accesstokens.TokenResponse{}, fmt.Errorf("error") 59 | } 60 | return f.AccessToken, nil 61 | } 62 | func (f *AccessTokens) FromAuthCode(ctx context.Context, req accesstokens.AuthCodeRequest) (accesstokens.TokenResponse, error) { 63 | if f.Err { 64 | return accesstokens.TokenResponse{}, fmt.Errorf("error") 65 | } 66 | return f.AccessToken, nil 67 | } 68 | func (f *AccessTokens) FromRefreshToken(ctx context.Context, appType accesstokens.AppType, authParams authority.AuthParams, cc *accesstokens.Credential, refreshToken string) (accesstokens.TokenResponse, error) { 69 | if f.FromRefreshTokenCallback != nil { 70 | f.FromRefreshTokenCallback(appType, authParams, cc, refreshToken) 71 | } 72 | if f.Err { 73 | return accesstokens.TokenResponse{}, fmt.Errorf("error") 74 | } 75 | return f.AccessToken, nil 76 | } 77 | func (f *AccessTokens) FromClientSecret(ctx context.Context, authParameters authority.AuthParams, clientSecret string) (accesstokens.TokenResponse, error) { 78 | if f.Err { 79 | return accesstokens.TokenResponse{}, fmt.Errorf("error") 80 | } 81 | return f.AccessToken, nil 82 | } 83 | func (f *AccessTokens) FromAssertion(ctx context.Context, authParameters authority.AuthParams, assertion string) (accesstokens.TokenResponse, error) { 84 | if f.Err { 85 | return accesstokens.TokenResponse{}, fmt.Errorf("error") 86 | } 87 | if f.ValidateAssertion != nil { 88 | f.ValidateAssertion(assertion) 89 | } 90 | return f.AccessToken, nil 91 | } 92 | func (f *AccessTokens) FromUserAssertionClientSecret(ctx context.Context, authParameters authority.AuthParams, userAssertion, clientSecret string) (accesstokens.TokenResponse, error) { 93 | if f.Err { 94 | return accesstokens.TokenResponse{}, fmt.Errorf("error") 95 | } 96 | return f.AccessToken, nil 97 | } 98 | func (f *AccessTokens) FromUserAssertionClientCertificate(ctx context.Context, authParameters authority.AuthParams, userAssertion, assertion string) (accesstokens.TokenResponse, error) { 99 | if f.Err { 100 | return accesstokens.TokenResponse{}, fmt.Errorf("error") 101 | } 102 | return f.AccessToken, nil 103 | } 104 | func (f *AccessTokens) DeviceCodeResult(ctx context.Context, authParameters authority.AuthParams) (accesstokens.DeviceCodeResult, error) { 105 | if f.Err { 106 | return accesstokens.DeviceCodeResult{}, fmt.Errorf("error") 107 | } 108 | return f.DeviceCode, nil 109 | } 110 | func (f *AccessTokens) FromDeviceCodeResult(ctx context.Context, authParameters authority.AuthParams, deviceCodeResult accesstokens.DeviceCodeResult) (accesstokens.TokenResponse, error) { 111 | if f.Next < len(f.Result) { 112 | defer func() { f.Next++ }() 113 | v := f.Result[f.Next] 114 | if v == nil { 115 | return f.AccessToken, nil 116 | } 117 | return accesstokens.TokenResponse{}, v 118 | } 119 | panic("AccessTokens.FromDeviceCodeResult() asked for more return values than provided") 120 | } 121 | func (f *AccessTokens) FromSamlGrant(ctx context.Context, authParameters authority.AuthParams, samlGrant wstrust.SamlTokenInfo) (accesstokens.TokenResponse, error) { 122 | if f.Err { 123 | return accesstokens.TokenResponse{}, fmt.Errorf("error") 124 | } 125 | return f.AccessToken, nil 126 | } 127 | 128 | // Authority is a fake implementation of the oauth.fetchAuthority interface. 129 | type Authority struct { 130 | // Set this to true to have all APIs return an error. 131 | Err bool 132 | 133 | // The fake UserRealm to return from the UserRealm() API. 134 | Realm authority.UserRealm 135 | 136 | // fake result to return 137 | InstanceResp authority.InstanceDiscoveryResponse 138 | } 139 | 140 | func (f Authority) UserRealm(ctx context.Context, params authority.AuthParams) (authority.UserRealm, error) { 141 | if f.Err { 142 | return authority.UserRealm{}, errors.New("error") 143 | } 144 | return f.Realm, nil 145 | } 146 | 147 | func (f Authority) AADInstanceDiscovery(ctx context.Context, info authority.Info) (authority.InstanceDiscoveryResponse, error) { 148 | if f.Err { 149 | return authority.InstanceDiscoveryResponse{}, errors.New("error") 150 | } 151 | return f.InstanceResp, nil 152 | } 153 | 154 | // WSTrust is a fake implementation of the oauth.fetchWSTrust interface. 155 | type WSTrust struct { 156 | // Set these to true to have their respective APIs return an error. 157 | GetMexErr, GetSAMLTokenInfoErr bool 158 | 159 | // fake result to return 160 | MexDocument defs.MexDocument 161 | 162 | // fake result to return 163 | SamlTokenInfo wstrust.SamlTokenInfo 164 | } 165 | 166 | func (f WSTrust) Mex(ctx context.Context, federationMetadataURL string) (defs.MexDocument, error) { 167 | if f.GetMexErr { 168 | return defs.MexDocument{}, errors.New("error") 169 | } 170 | return f.MexDocument, nil 171 | } 172 | 173 | func (f WSTrust) SAMLTokenInfo(ctx context.Context, authParameters authority.AuthParams, cloudAudienceURN string, endpoint defs.Endpoint) (wstrust.SamlTokenInfo, error) { 174 | if f.GetSAMLTokenInfoErr { 175 | return wstrust.SamlTokenInfo{}, errors.New("error") 176 | } 177 | return f.SamlTokenInfo, nil 178 | } 179 | -------------------------------------------------------------------------------- /apps/internal/oauth/ops/accesstokens/apptype_string.go: -------------------------------------------------------------------------------- 1 | // Code generated by "stringer -type=AppType"; DO NOT EDIT. 2 | 3 | package accesstokens 4 | 5 | import "strconv" 6 | 7 | func _() { 8 | // An "invalid array index" compiler error signifies that the constant values have changed. 9 | // Re-run the stringer command to generate them again. 10 | var x [1]struct{} 11 | _ = x[ATUnknown-0] 12 | _ = x[ATPublic-1] 13 | _ = x[ATConfidential-2] 14 | } 15 | 16 | const _AppType_name = "ATUnknownATPublicATConfidential" 17 | 18 | var _AppType_index = [...]uint8{0, 9, 17, 31} 19 | 20 | func (i AppType) String() string { 21 | if i < 0 || i >= AppType(len(_AppType_index)-1) { 22 | return "AppType(" + strconv.FormatInt(int64(i), 10) + ")" 23 | } 24 | return _AppType_name[_AppType_index[i]:_AppType_index[i+1]] 25 | } 26 | -------------------------------------------------------------------------------- /apps/internal/oauth/ops/authority/authorizetype_string.go: -------------------------------------------------------------------------------- 1 | // Code generated by "stringer -type=AuthorizeType"; DO NOT EDIT. 2 | 3 | package authority 4 | 5 | import "strconv" 6 | 7 | func _() { 8 | // An "invalid array index" compiler error signifies that the constant values have changed. 9 | // Re-run the stringer command to generate them again. 10 | var x [1]struct{} 11 | _ = x[ATUnknown-0] 12 | _ = x[ATUsernamePassword-1] 13 | _ = x[ATWindowsIntegrated-2] 14 | _ = x[ATAuthCode-3] 15 | _ = x[ATInteractive-4] 16 | _ = x[ATClientCredentials-5] 17 | _ = x[ATDeviceCode-6] 18 | _ = x[ATRefreshToken-7] 19 | } 20 | 21 | const _AuthorizeType_name = "ATUnknownATUsernamePasswordATWindowsIntegratedATAuthCodeATInteractiveATClientCredentialsATDeviceCodeATRefreshToken" 22 | 23 | var _AuthorizeType_index = [...]uint8{0, 9, 27, 46, 56, 69, 88, 100, 114} 24 | 25 | func (i AuthorizeType) String() string { 26 | if i < 0 || i >= AuthorizeType(len(_AuthorizeType_index)-1) { 27 | return "AuthorizeType(" + strconv.FormatInt(int64(i), 10) + ")" 28 | } 29 | return _AuthorizeType_name[_AuthorizeType_index[i]:_AuthorizeType_index[i+1]] 30 | } 31 | -------------------------------------------------------------------------------- /apps/internal/oauth/ops/internal/comm/compress.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | package comm 5 | 6 | import ( 7 | "compress/gzip" 8 | "io" 9 | ) 10 | 11 | func gzipDecompress(r io.Reader) io.Reader { 12 | gzipReader, _ := gzip.NewReader(r) 13 | 14 | pipeOut, pipeIn := io.Pipe() 15 | go func() { 16 | // decompression bomb would have to come from Azure services. 17 | // If we want to limit, we should do that in comm.do(). 18 | _, err := io.Copy(pipeIn, gzipReader) //nolint 19 | if err != nil { 20 | // don't need the error. 21 | pipeIn.CloseWithError(err) //nolint 22 | gzipReader.Close() 23 | return 24 | } 25 | if err := gzipReader.Close(); err != nil { 26 | // don't need the error. 27 | pipeIn.CloseWithError(err) //nolint 28 | return 29 | } 30 | pipeIn.Close() 31 | }() 32 | return pipeOut 33 | } 34 | -------------------------------------------------------------------------------- /apps/internal/oauth/ops/internal/grant/grant.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | // Package grant holds types of grants issued by authorization services. 5 | package grant 6 | 7 | const ( 8 | Password = "password" 9 | JWT = "urn:ietf:params:oauth:grant-type:jwt-bearer" 10 | SAMLV1 = "urn:ietf:params:oauth:grant-type:saml1_1-bearer" 11 | SAMLV2 = "urn:ietf:params:oauth:grant-type:saml2-bearer" 12 | DeviceCode = "device_code" 13 | AuthCode = "authorization_code" 14 | RefreshToken = "refresh_token" 15 | ClientCredential = "client_credentials" 16 | ClientAssertion = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer" 17 | ) 18 | -------------------------------------------------------------------------------- /apps/internal/oauth/ops/ops.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | /* 5 | Package ops provides operations to various backend services using REST clients. 6 | 7 | The REST type provides several clients that can be used to communicate to backends. 8 | Usage is simple: 9 | 10 | rest := ops.New() 11 | 12 | // Creates an authority client and calls the UserRealm() method. 13 | userRealm, err := rest.Authority().UserRealm(ctx, authParameters) 14 | if err != nil { 15 | // Do something 16 | } 17 | */ 18 | package ops 19 | 20 | import ( 21 | "github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/oauth/ops/accesstokens" 22 | "github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/oauth/ops/authority" 23 | "github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/oauth/ops/internal/comm" 24 | "github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/oauth/ops/wstrust" 25 | ) 26 | 27 | // HTTPClient represents an HTTP client. 28 | // It's usually an *http.Client from the standard library. 29 | type HTTPClient = comm.HTTPClient 30 | 31 | // REST provides REST clients for communicating with various backends used by MSAL. 32 | type REST struct { 33 | client *comm.Client 34 | } 35 | 36 | // New is the constructor for REST. 37 | func New(httpClient HTTPClient) *REST { 38 | return &REST{client: comm.New(httpClient)} 39 | } 40 | 41 | // Authority returns a client for querying information about various authorities. 42 | func (r *REST) Authority() authority.Client { 43 | return authority.Client{Comm: r.client} 44 | } 45 | 46 | // AccessTokens returns a client that can be used to get various access tokens for 47 | // authorization purposes. 48 | func (r *REST) AccessTokens() accesstokens.Client { 49 | return accesstokens.Client{Comm: r.client} 50 | } 51 | 52 | // WSTrust provides access to various metadata in a WSTrust service. This data can 53 | // be used to gain tokens based on SAML data using the client provided by AccessTokens(). 54 | func (r *REST) WSTrust() wstrust.Client { 55 | return wstrust.Client{Comm: r.client} 56 | } 57 | -------------------------------------------------------------------------------- /apps/internal/oauth/ops/wstrust/defs/endpointtype_string.go: -------------------------------------------------------------------------------- 1 | // Code generated by "stringer -type=endpointType"; DO NOT EDIT. 2 | 3 | package defs 4 | 5 | import "strconv" 6 | 7 | func _() { 8 | // An "invalid array index" compiler error signifies that the constant values have changed. 9 | // Re-run the stringer command to generate them again. 10 | var x [1]struct{} 11 | _ = x[etUnknown-0] 12 | _ = x[etUsernamePassword-1] 13 | _ = x[etWindowsTransport-2] 14 | } 15 | 16 | const _endpointType_name = "etUnknownetUsernamePasswordetWindowsTransport" 17 | 18 | var _endpointType_index = [...]uint8{0, 9, 27, 45} 19 | 20 | func (i endpointType) String() string { 21 | if i < 0 || i >= endpointType(len(_endpointType_index)-1) { 22 | return "endpointType(" + strconv.FormatInt(int64(i), 10) + ")" 23 | } 24 | return _endpointType_name[_endpointType_index[i]:_endpointType_index[i+1]] 25 | } 26 | -------------------------------------------------------------------------------- /apps/internal/oauth/ops/wstrust/defs/version_string.go: -------------------------------------------------------------------------------- 1 | // Code generated by "stringer -type=Version"; DO NOT EDIT. 2 | 3 | package defs 4 | 5 | import "strconv" 6 | 7 | func _() { 8 | // An "invalid array index" compiler error signifies that the constant values have changed. 9 | // Re-run the stringer command to generate them again. 10 | var x [1]struct{} 11 | _ = x[TrustUnknown-0] 12 | _ = x[Trust2005-1] 13 | _ = x[Trust13-2] 14 | } 15 | 16 | const _Version_name = "TrustUnknownTrust2005Trust13" 17 | 18 | var _Version_index = [...]uint8{0, 12, 21, 28} 19 | 20 | func (i Version) String() string { 21 | if i < 0 || i >= Version(len(_Version_index)-1) { 22 | return "Version(" + strconv.FormatInt(int64(i), 10) + ")" 23 | } 24 | return _Version_name[_Version_index[i]:_Version_index[i+1]] 25 | } 26 | -------------------------------------------------------------------------------- /apps/internal/oauth/ops/wstrust/defs/wstrust_endpoint.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | package defs 5 | 6 | import ( 7 | "encoding/xml" 8 | "fmt" 9 | "time" 10 | 11 | "github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/oauth/ops/authority" 12 | uuid "github.com/google/uuid" 13 | ) 14 | 15 | //go:generate stringer -type=Version 16 | 17 | type Version int 18 | 19 | const ( 20 | TrustUnknown Version = iota 21 | Trust2005 22 | Trust13 23 | ) 24 | 25 | // Endpoint represents a WSTrust endpoint. 26 | type Endpoint struct { 27 | // Version is the version of the endpoint. 28 | Version Version 29 | // URL is the URL of the endpoint. 30 | URL string 31 | } 32 | 33 | type wsTrustTokenRequestEnvelope struct { 34 | XMLName xml.Name `xml:"s:Envelope"` 35 | Text string `xml:",chardata"` 36 | S string `xml:"xmlns:s,attr"` 37 | Wsa string `xml:"xmlns:wsa,attr"` 38 | Wsu string `xml:"xmlns:wsu,attr"` 39 | Header struct { 40 | Text string `xml:",chardata"` 41 | Action struct { 42 | Text string `xml:",chardata"` 43 | MustUnderstand string `xml:"s:mustUnderstand,attr"` 44 | } `xml:"wsa:Action"` 45 | MessageID struct { 46 | Text string `xml:",chardata"` 47 | } `xml:"wsa:messageID"` 48 | ReplyTo struct { 49 | Text string `xml:",chardata"` 50 | Address struct { 51 | Text string `xml:",chardata"` 52 | } `xml:"wsa:Address"` 53 | } `xml:"wsa:ReplyTo"` 54 | To struct { 55 | Text string `xml:",chardata"` 56 | MustUnderstand string `xml:"s:mustUnderstand,attr"` 57 | } `xml:"wsa:To"` 58 | Security struct { 59 | Text string `xml:",chardata"` 60 | MustUnderstand string `xml:"s:mustUnderstand,attr"` 61 | Wsse string `xml:"xmlns:wsse,attr"` 62 | Timestamp struct { 63 | Text string `xml:",chardata"` 64 | ID string `xml:"wsu:Id,attr"` 65 | Created struct { 66 | Text string `xml:",chardata"` 67 | } `xml:"wsu:Created"` 68 | Expires struct { 69 | Text string `xml:",chardata"` 70 | } `xml:"wsu:Expires"` 71 | } `xml:"wsu:Timestamp"` 72 | UsernameToken struct { 73 | Text string `xml:",chardata"` 74 | ID string `xml:"wsu:Id,attr"` 75 | Username struct { 76 | Text string `xml:",chardata"` 77 | } `xml:"wsse:Username"` 78 | Password struct { 79 | Text string `xml:",chardata"` 80 | } `xml:"wsse:Password"` 81 | } `xml:"wsse:UsernameToken"` 82 | } `xml:"wsse:Security"` 83 | } `xml:"s:Header"` 84 | Body struct { 85 | Text string `xml:",chardata"` 86 | RequestSecurityToken struct { 87 | Text string `xml:",chardata"` 88 | Wst string `xml:"xmlns:wst,attr"` 89 | AppliesTo struct { 90 | Text string `xml:",chardata"` 91 | Wsp string `xml:"xmlns:wsp,attr"` 92 | EndpointReference struct { 93 | Text string `xml:",chardata"` 94 | Address struct { 95 | Text string `xml:",chardata"` 96 | } `xml:"wsa:Address"` 97 | } `xml:"wsa:EndpointReference"` 98 | } `xml:"wsp:AppliesTo"` 99 | KeyType struct { 100 | Text string `xml:",chardata"` 101 | } `xml:"wst:KeyType"` 102 | RequestType struct { 103 | Text string `xml:",chardata"` 104 | } `xml:"wst:RequestType"` 105 | } `xml:"wst:RequestSecurityToken"` 106 | } `xml:"s:Body"` 107 | } 108 | 109 | func buildTimeString(t time.Time) string { 110 | // Golang time formats are weird: https://stackoverflow.com/questions/20234104/how-to-format-current-time-using-a-yyyymmddhhmmss-format 111 | return t.Format("2006-01-02T15:04:05.000Z") 112 | } 113 | 114 | func (wte *Endpoint) buildTokenRequestMessage(authType authority.AuthorizeType, cloudAudienceURN string, username string, password string) (string, error) { 115 | var soapAction string 116 | var trustNamespace string 117 | var keyType string 118 | var requestType string 119 | 120 | createdTime := time.Now().UTC() 121 | expiresTime := createdTime.Add(10 * time.Minute) 122 | 123 | switch wte.Version { 124 | case Trust2005: 125 | soapAction = trust2005Spec 126 | trustNamespace = "http://schemas.xmlsoap.org/ws/2005/02/trust" 127 | keyType = "http://schemas.xmlsoap.org/ws/2005/05/identity/NoProofKey" 128 | requestType = "http://schemas.xmlsoap.org/ws/2005/02/trust/Issue" 129 | case Trust13: 130 | soapAction = trust13Spec 131 | trustNamespace = "http://docs.oasis-open.org/ws-sx/ws-trust/200512" 132 | keyType = "http://docs.oasis-open.org/ws-sx/ws-trust/200512/Bearer" 133 | requestType = "http://docs.oasis-open.org/ws-sx/ws-trust/200512/Issue" 134 | default: 135 | return "", fmt.Errorf("buildTokenRequestMessage had Version == %q, which is not recognized", wte.Version) 136 | } 137 | 138 | var envelope wsTrustTokenRequestEnvelope 139 | 140 | messageUUID := uuid.New() 141 | 142 | envelope.S = "http://www.w3.org/2003/05/soap-envelope" 143 | envelope.Wsa = "http://www.w3.org/2005/08/addressing" 144 | envelope.Wsu = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd" 145 | 146 | envelope.Header.Action.MustUnderstand = "1" 147 | envelope.Header.Action.Text = soapAction 148 | envelope.Header.MessageID.Text = "urn:uuid:" + messageUUID.String() 149 | envelope.Header.ReplyTo.Address.Text = "http://www.w3.org/2005/08/addressing/anonymous" 150 | envelope.Header.To.MustUnderstand = "1" 151 | envelope.Header.To.Text = wte.URL 152 | 153 | switch authType { 154 | case authority.ATUnknown: 155 | return "", fmt.Errorf("buildTokenRequestMessage had no authority type(%v)", authType) 156 | case authority.ATUsernamePassword: 157 | endpointUUID := uuid.New() 158 | 159 | var trustID string 160 | if wte.Version == Trust2005 { 161 | trustID = "UnPwSecTok2005-" + endpointUUID.String() 162 | } else { 163 | trustID = "UnPwSecTok13-" + endpointUUID.String() 164 | } 165 | 166 | envelope.Header.Security.MustUnderstand = "1" 167 | envelope.Header.Security.Wsse = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" 168 | envelope.Header.Security.Timestamp.ID = "MSATimeStamp" 169 | envelope.Header.Security.Timestamp.Created.Text = buildTimeString(createdTime) 170 | envelope.Header.Security.Timestamp.Expires.Text = buildTimeString(expiresTime) 171 | envelope.Header.Security.UsernameToken.ID = trustID 172 | envelope.Header.Security.UsernameToken.Username.Text = username 173 | envelope.Header.Security.UsernameToken.Password.Text = password 174 | default: 175 | // This is just to note that we don't do anything for other cases. 176 | // We aren't missing anything I know of. 177 | } 178 | 179 | envelope.Body.RequestSecurityToken.Wst = trustNamespace 180 | envelope.Body.RequestSecurityToken.AppliesTo.Wsp = "http://schemas.xmlsoap.org/ws/2004/09/policy" 181 | envelope.Body.RequestSecurityToken.AppliesTo.EndpointReference.Address.Text = cloudAudienceURN 182 | envelope.Body.RequestSecurityToken.KeyType.Text = keyType 183 | envelope.Body.RequestSecurityToken.RequestType.Text = requestType 184 | 185 | output, err := xml.Marshal(envelope) 186 | if err != nil { 187 | return "", err 188 | } 189 | 190 | return string(output), nil 191 | } 192 | 193 | func (wte *Endpoint) BuildTokenRequestMessageWIA(cloudAudienceURN string) (string, error) { 194 | return wte.buildTokenRequestMessage(authority.ATWindowsIntegrated, cloudAudienceURN, "", "") 195 | } 196 | 197 | func (wte *Endpoint) BuildTokenRequestMessageUsernamePassword(cloudAudienceURN string, username string, password string) (string, error) { 198 | return wte.buildTokenRequestMessage(authority.ATUsernamePassword, cloudAudienceURN, username, password) 199 | } 200 | -------------------------------------------------------------------------------- /apps/internal/oauth/ops/wstrust/defs/wstrust_mex_document.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | package defs 5 | 6 | import ( 7 | "errors" 8 | "fmt" 9 | "strings" 10 | ) 11 | 12 | //go:generate stringer -type=endpointType 13 | 14 | type endpointType int 15 | 16 | const ( 17 | etUnknown endpointType = iota 18 | etUsernamePassword 19 | etWindowsTransport 20 | ) 21 | 22 | type wsEndpointData struct { 23 | Version Version 24 | EndpointType endpointType 25 | } 26 | 27 | const trust13Spec string = "http://docs.oasis-open.org/ws-sx/ws-trust/200512/RST/Issue" 28 | const trust2005Spec string = "http://schemas.xmlsoap.org/ws/2005/02/trust/RST/Issue" 29 | 30 | type MexDocument struct { 31 | UsernamePasswordEndpoint Endpoint 32 | WindowsTransportEndpoint Endpoint 33 | policies map[string]endpointType 34 | bindings map[string]wsEndpointData 35 | } 36 | 37 | func updateEndpoint(cached *Endpoint, found Endpoint) { 38 | if cached == nil || cached.Version == TrustUnknown { 39 | *cached = found 40 | return 41 | } 42 | if (*cached).Version == Trust2005 && found.Version == Trust13 { 43 | *cached = found 44 | return 45 | } 46 | } 47 | 48 | // TODO(msal): Someone needs to write tests for everything below. 49 | 50 | // NewFromDef creates a new MexDocument. 51 | func NewFromDef(defs Definitions) (MexDocument, error) { 52 | policies, err := policies(defs) 53 | if err != nil { 54 | return MexDocument{}, err 55 | } 56 | 57 | bindings, err := bindings(defs, policies) 58 | if err != nil { 59 | return MexDocument{}, err 60 | } 61 | 62 | userPass, windows, err := endpoints(defs, bindings) 63 | if err != nil { 64 | return MexDocument{}, err 65 | } 66 | 67 | return MexDocument{ 68 | UsernamePasswordEndpoint: userPass, 69 | WindowsTransportEndpoint: windows, 70 | policies: policies, 71 | bindings: bindings, 72 | }, nil 73 | } 74 | 75 | func policies(defs Definitions) (map[string]endpointType, error) { 76 | policies := make(map[string]endpointType, len(defs.Policy)) 77 | 78 | for _, policy := range defs.Policy { 79 | if policy.ExactlyOne.All.NegotiateAuthentication.XMLName.Local != "" { 80 | if policy.ExactlyOne.All.TransportBinding.SP != "" && policy.ID != "" { 81 | policies["#"+policy.ID] = etWindowsTransport 82 | } 83 | } 84 | 85 | if policy.ExactlyOne.All.SignedEncryptedSupportingTokens.Policy.UsernameToken.Policy.WSSUsernameToken10.XMLName.Local != "" { 86 | if policy.ExactlyOne.All.TransportBinding.SP != "" && policy.ID != "" { 87 | policies["#"+policy.ID] = etUsernamePassword 88 | } 89 | } 90 | if policy.ExactlyOne.All.SignedSupportingTokens.Policy.UsernameToken.Policy.WSSUsernameToken10.XMLName.Local != "" { 91 | if policy.ExactlyOne.All.TransportBinding.SP != "" && policy.ID != "" { 92 | policies["#"+policy.ID] = etUsernamePassword 93 | } 94 | } 95 | } 96 | 97 | if len(policies) == 0 { 98 | return policies, errors.New("no policies for mex document") 99 | } 100 | 101 | return policies, nil 102 | } 103 | 104 | func bindings(defs Definitions, policies map[string]endpointType) (map[string]wsEndpointData, error) { 105 | bindings := make(map[string]wsEndpointData, len(defs.Binding)) 106 | 107 | for _, binding := range defs.Binding { 108 | policyName := binding.PolicyReference.URI 109 | transport := binding.Binding.Transport 110 | 111 | if transport == "http://schemas.xmlsoap.org/soap/http" { 112 | if policy, ok := policies[policyName]; ok { 113 | bindingName := binding.Name 114 | specVersion := binding.Operation.Operation.SoapAction 115 | 116 | if specVersion == trust13Spec { 117 | bindings[bindingName] = wsEndpointData{Trust13, policy} 118 | } else if specVersion == trust2005Spec { 119 | bindings[bindingName] = wsEndpointData{Trust2005, policy} 120 | } else { 121 | return nil, errors.New("found unknown spec version in mex document") 122 | } 123 | } 124 | } 125 | } 126 | return bindings, nil 127 | } 128 | 129 | func endpoints(defs Definitions, bindings map[string]wsEndpointData) (userPass, windows Endpoint, err error) { 130 | for _, port := range defs.Service.Port { 131 | bindingName := port.Binding 132 | 133 | index := strings.Index(bindingName, ":") 134 | if index != -1 { 135 | bindingName = bindingName[index+1:] 136 | } 137 | 138 | if binding, ok := bindings[bindingName]; ok { 139 | url := strings.TrimSpace(port.EndpointReference.Address.Text) 140 | if url == "" { 141 | return Endpoint{}, Endpoint{}, fmt.Errorf("MexDocument cannot have blank URL endpoint") 142 | } 143 | if binding.Version == TrustUnknown { 144 | return Endpoint{}, Endpoint{}, fmt.Errorf("endpoint version unknown") 145 | } 146 | endpoint := Endpoint{Version: binding.Version, URL: url} 147 | 148 | switch binding.EndpointType { 149 | case etUsernamePassword: 150 | updateEndpoint(&userPass, endpoint) 151 | case etWindowsTransport: 152 | updateEndpoint(&windows, endpoint) 153 | default: 154 | return Endpoint{}, Endpoint{}, errors.New("found unknown port type in MEX document") 155 | } 156 | } 157 | } 158 | return userPass, windows, nil 159 | } 160 | -------------------------------------------------------------------------------- /apps/internal/oauth/ops/wstrust/wstrust.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | /* 5 | Package wstrust provides a client for communicating with a WSTrust (https://en.wikipedia.org/wiki/WS-Trust#:~:text=WS%2DTrust%20is%20a%20WS,in%20a%20secure%20message%20exchange.) 6 | for the purposes of extracting metadata from the service. This data can be used to acquire 7 | tokens using the accesstokens.Client.GetAccessTokenFromSamlGrant() call. 8 | */ 9 | package wstrust 10 | 11 | import ( 12 | "context" 13 | "errors" 14 | "fmt" 15 | "net/http" 16 | "net/url" 17 | 18 | "github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/oauth/ops/authority" 19 | "github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/oauth/ops/internal/grant" 20 | "github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/oauth/ops/wstrust/defs" 21 | ) 22 | 23 | type xmlCaller interface { 24 | XMLCall(ctx context.Context, endpoint string, headers http.Header, qv url.Values, resp interface{}) error 25 | SOAPCall(ctx context.Context, endpoint, action string, headers http.Header, qv url.Values, body string, resp interface{}) error 26 | } 27 | 28 | type SamlTokenInfo struct { 29 | AssertionType string // Should be either constants SAMLV1Grant or SAMLV2Grant. 30 | Assertion string 31 | } 32 | 33 | // Client represents the REST calls to get tokens from token generator backends. 34 | type Client struct { 35 | // Comm provides the HTTP transport client. 36 | Comm xmlCaller 37 | } 38 | 39 | // TODO(msal): This allows me to call Mex without having a real Def file on line 45. 40 | // This would fail because policies() would not find a policy. This is easy enough to 41 | // fix in test data, but.... Definitions is defined with built in structs. That needs 42 | // to be pulled apart and until then I have this hack in. 43 | var newFromDef = defs.NewFromDef 44 | 45 | // Mex provides metadata about a wstrust service. 46 | func (c Client) Mex(ctx context.Context, federationMetadataURL string) (defs.MexDocument, error) { 47 | resp := defs.Definitions{} 48 | err := c.Comm.XMLCall( 49 | ctx, 50 | federationMetadataURL, 51 | http.Header{}, 52 | nil, 53 | &resp, 54 | ) 55 | if err != nil { 56 | return defs.MexDocument{}, err 57 | } 58 | 59 | return newFromDef(resp) 60 | } 61 | 62 | const ( 63 | SoapActionDefault = "http://docs.oasis-open.org/ws-sx/ws-trust/200512/RST/Issue" 64 | 65 | // Note: Commented out because this action is not supported. It was in the original code 66 | // but only used in a switch where it errored. Since there was only one value, a default 67 | // worked better. However, buildTokenRequestMessage() had 2005 support. I'm not actually 68 | // sure what's going on here. It like we have half support. For now this is here just 69 | // for documentation purposes in case we are going to add support. 70 | // 71 | // SoapActionWSTrust2005 = "http://schemas.xmlsoap.org/ws/2005/02/trust/RST/Issue" 72 | ) 73 | 74 | // SAMLTokenInfo provides SAML information that is used to generate a SAML token. 75 | func (c Client) SAMLTokenInfo(ctx context.Context, authParameters authority.AuthParams, cloudAudienceURN string, endpoint defs.Endpoint) (SamlTokenInfo, error) { 76 | var wsTrustRequestMessage string 77 | var err error 78 | 79 | switch authParameters.AuthorizationType { 80 | case authority.ATWindowsIntegrated: 81 | wsTrustRequestMessage, err = endpoint.BuildTokenRequestMessageWIA(cloudAudienceURN) 82 | if err != nil { 83 | return SamlTokenInfo{}, err 84 | } 85 | case authority.ATUsernamePassword: 86 | wsTrustRequestMessage, err = endpoint.BuildTokenRequestMessageUsernamePassword( 87 | cloudAudienceURN, authParameters.Username, authParameters.Password) 88 | if err != nil { 89 | return SamlTokenInfo{}, err 90 | } 91 | default: 92 | return SamlTokenInfo{}, fmt.Errorf("unknown auth type %v", authParameters.AuthorizationType) 93 | } 94 | 95 | var soapAction string 96 | switch endpoint.Version { 97 | case defs.Trust13: 98 | soapAction = SoapActionDefault 99 | case defs.Trust2005: 100 | return SamlTokenInfo{}, errors.New("WS Trust 2005 support is not implemented") 101 | default: 102 | return SamlTokenInfo{}, fmt.Errorf("the SOAP endpoint for a wstrust call had an invalid version: %v", endpoint.Version) 103 | } 104 | 105 | resp := defs.SAMLDefinitions{} 106 | err = c.Comm.SOAPCall(ctx, endpoint.URL, soapAction, http.Header{}, nil, wsTrustRequestMessage, &resp) 107 | if err != nil { 108 | return SamlTokenInfo{}, err 109 | } 110 | 111 | return c.samlAssertion(resp) 112 | } 113 | 114 | const ( 115 | samlv1Assertion = "urn:oasis:names:tc:SAML:1.0:assertion" 116 | samlv2Assertion = "urn:oasis:names:tc:SAML:2.0:assertion" 117 | ) 118 | 119 | func (c Client) samlAssertion(def defs.SAMLDefinitions) (SamlTokenInfo, error) { 120 | for _, tokenResponse := range def.Body.RequestSecurityTokenResponseCollection.RequestSecurityTokenResponse { 121 | token := tokenResponse.RequestedSecurityToken 122 | if token.Assertion.XMLName.Local != "" { 123 | assertion := token.AssertionRawXML 124 | 125 | samlVersion := token.Assertion.Saml 126 | switch samlVersion { 127 | case samlv1Assertion: 128 | return SamlTokenInfo{AssertionType: grant.SAMLV1, Assertion: assertion}, nil 129 | case samlv2Assertion: 130 | return SamlTokenInfo{AssertionType: grant.SAMLV2, Assertion: assertion}, nil 131 | } 132 | return SamlTokenInfo{}, fmt.Errorf("couldn't parse SAML assertion, version unknown: %q", samlVersion) 133 | } 134 | } 135 | return SamlTokenInfo{}, errors.New("unknown WS-Trust version") 136 | } 137 | -------------------------------------------------------------------------------- /apps/internal/oauth/resolvers.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | // TODO(msal): Write some tests. The original code this came from didn't have tests and I'm too 5 | // tired at this point to do it. It, like many other *Manager code I found was broken because 6 | // they didn't have mutex protection. 7 | 8 | package oauth 9 | 10 | import ( 11 | "context" 12 | "errors" 13 | "fmt" 14 | "strings" 15 | "sync" 16 | 17 | "github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/oauth/ops" 18 | "github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/oauth/ops/authority" 19 | ) 20 | 21 | type cacheEntry struct { 22 | Endpoints authority.Endpoints 23 | ValidForDomainsInList map[string]bool 24 | } 25 | 26 | func createcacheEntry(endpoints authority.Endpoints) cacheEntry { 27 | return cacheEntry{endpoints, map[string]bool{}} 28 | } 29 | 30 | // AuthorityEndpoint retrieves endpoints from an authority for auth and token acquisition. 31 | type authorityEndpoint struct { 32 | rest *ops.REST 33 | 34 | mu sync.Mutex 35 | cache map[string]cacheEntry 36 | } 37 | 38 | // newAuthorityEndpoint is the constructor for AuthorityEndpoint. 39 | func newAuthorityEndpoint(rest *ops.REST) *authorityEndpoint { 40 | m := &authorityEndpoint{rest: rest, cache: map[string]cacheEntry{}} 41 | return m 42 | } 43 | 44 | // ResolveEndpoints gets the authorization and token endpoints and creates an AuthorityEndpoints instance 45 | func (m *authorityEndpoint) ResolveEndpoints(ctx context.Context, authorityInfo authority.Info, userPrincipalName string) (authority.Endpoints, error) { 46 | 47 | if endpoints, found := m.cachedEndpoints(authorityInfo, userPrincipalName); found { 48 | return endpoints, nil 49 | } 50 | 51 | endpoint, err := m.openIDConfigurationEndpoint(ctx, authorityInfo) 52 | if err != nil { 53 | return authority.Endpoints{}, err 54 | } 55 | 56 | resp, err := m.rest.Authority().GetTenantDiscoveryResponse(ctx, endpoint) 57 | if err != nil { 58 | return authority.Endpoints{}, err 59 | } 60 | if err := resp.Validate(); err != nil { 61 | return authority.Endpoints{}, fmt.Errorf("ResolveEndpoints(): %w", err) 62 | } 63 | 64 | tenant := authorityInfo.Tenant 65 | 66 | endpoints := authority.NewEndpoints( 67 | strings.Replace(resp.AuthorizationEndpoint, "{tenant}", tenant, -1), 68 | strings.Replace(resp.TokenEndpoint, "{tenant}", tenant, -1), 69 | strings.Replace(resp.Issuer, "{tenant}", tenant, -1), 70 | authorityInfo.Host) 71 | 72 | m.addCachedEndpoints(authorityInfo, userPrincipalName, endpoints) 73 | 74 | return endpoints, nil 75 | } 76 | 77 | // cachedEndpoints returns a the cached endpoints if they exists. If not, we return false. 78 | func (m *authorityEndpoint) cachedEndpoints(authorityInfo authority.Info, userPrincipalName string) (authority.Endpoints, bool) { 79 | m.mu.Lock() 80 | defer m.mu.Unlock() 81 | 82 | if cacheEntry, ok := m.cache[authorityInfo.CanonicalAuthorityURI]; ok { 83 | if authorityInfo.AuthorityType == authority.ADFS { 84 | domain, err := adfsDomainFromUpn(userPrincipalName) 85 | if err == nil { 86 | if _, ok := cacheEntry.ValidForDomainsInList[domain]; ok { 87 | return cacheEntry.Endpoints, true 88 | } 89 | } 90 | } 91 | return cacheEntry.Endpoints, true 92 | } 93 | return authority.Endpoints{}, false 94 | } 95 | 96 | func (m *authorityEndpoint) addCachedEndpoints(authorityInfo authority.Info, userPrincipalName string, endpoints authority.Endpoints) { 97 | m.mu.Lock() 98 | defer m.mu.Unlock() 99 | 100 | updatedCacheEntry := createcacheEntry(endpoints) 101 | 102 | if authorityInfo.AuthorityType == authority.ADFS { 103 | // Since we're here, we've made a call to the backend. We want to ensure we're caching 104 | // the latest values from the server. 105 | if cacheEntry, ok := m.cache[authorityInfo.CanonicalAuthorityURI]; ok { 106 | for k := range cacheEntry.ValidForDomainsInList { 107 | updatedCacheEntry.ValidForDomainsInList[k] = true 108 | } 109 | } 110 | domain, err := adfsDomainFromUpn(userPrincipalName) 111 | if err == nil { 112 | updatedCacheEntry.ValidForDomainsInList[domain] = true 113 | } 114 | } 115 | 116 | m.cache[authorityInfo.CanonicalAuthorityURI] = updatedCacheEntry 117 | } 118 | 119 | func (m *authorityEndpoint) openIDConfigurationEndpoint(ctx context.Context, authorityInfo authority.Info) (string, error) { 120 | if authorityInfo.AuthorityType == authority.ADFS { 121 | return fmt.Sprintf("https://%s/adfs/.well-known/openid-configuration", authorityInfo.Host), nil 122 | } else if authorityInfo.AuthorityType == authority.DSTS { 123 | return fmt.Sprintf("https://%s/dstsv2/%s/v2.0/.well-known/openid-configuration", authorityInfo.Host, authority.DSTSTenant), nil 124 | 125 | } else if authorityInfo.ValidateAuthority && !authority.TrustedHost(authorityInfo.Host) { 126 | resp, err := m.rest.Authority().AADInstanceDiscovery(ctx, authorityInfo) 127 | if err != nil { 128 | return "", err 129 | } 130 | return resp.TenantDiscoveryEndpoint, nil 131 | } else if authorityInfo.Region != "" { 132 | resp, err := m.rest.Authority().AADInstanceDiscovery(ctx, authorityInfo) 133 | if err != nil { 134 | return "", err 135 | } 136 | return resp.TenantDiscoveryEndpoint, nil 137 | } 138 | 139 | return authorityInfo.CanonicalAuthorityURI + "v2.0/.well-known/openid-configuration", nil 140 | } 141 | 142 | func adfsDomainFromUpn(userPrincipalName string) (string, error) { 143 | parts := strings.Split(userPrincipalName, "@") 144 | if len(parts) < 2 { 145 | return "", errors.New("no @ present in user principal name") 146 | } 147 | return parts[1], nil 148 | } 149 | -------------------------------------------------------------------------------- /apps/internal/options/options.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | package options 5 | 6 | import ( 7 | "errors" 8 | "fmt" 9 | ) 10 | 11 | // CallOption implements an optional argument to a method call. See 12 | // https://blog.devgenius.io/go-call-option-that-can-be-used-with-multiple-methods-6c81734f3dbe 13 | // for an explanation of the usage pattern. 14 | type CallOption interface { 15 | Do(any) error 16 | callOption() 17 | } 18 | 19 | // ApplyOptions applies all the callOptions to options. options must be a pointer to a struct and 20 | // callOptions must be a list of objects that implement CallOption. 21 | func ApplyOptions[O, C any](options O, callOptions []C) error { 22 | for _, o := range callOptions { 23 | if t, ok := any(o).(CallOption); !ok { 24 | return fmt.Errorf("unexpected option type %T", o) 25 | } else if err := t.Do(options); err != nil { 26 | return err 27 | } 28 | } 29 | return nil 30 | } 31 | 32 | // NewCallOption returns a new CallOption whose Do() method calls function "f". 33 | func NewCallOption(f func(any) error) CallOption { 34 | if f == nil { 35 | // This isn't a practical concern because only an MSAL maintainer can get 36 | // us here, by implementing a do-nothing option. But if someone does that, 37 | // the below ensures the method invoked with the option returns an error. 38 | return callOption(func(any) error { 39 | return errors.New("invalid option: missing implementation") 40 | }) 41 | } 42 | return callOption(f) 43 | } 44 | 45 | // callOption is an adapter for a function to a CallOption 46 | type callOption func(any) error 47 | 48 | func (c callOption) Do(a any) error { 49 | return c(a) 50 | } 51 | 52 | func (callOption) callOption() {} 53 | -------------------------------------------------------------------------------- /apps/internal/shared/shared.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | package shared 5 | 6 | import ( 7 | "net/http" 8 | "reflect" 9 | "strings" 10 | ) 11 | 12 | const ( 13 | // CacheKeySeparator is used in creating the keys of the cache. 14 | CacheKeySeparator = "-" 15 | ) 16 | 17 | type Account struct { 18 | HomeAccountID string `json:"home_account_id,omitempty"` 19 | Environment string `json:"environment,omitempty"` 20 | Realm string `json:"realm,omitempty"` 21 | LocalAccountID string `json:"local_account_id,omitempty"` 22 | AuthorityType string `json:"authority_type,omitempty"` 23 | PreferredUsername string `json:"username,omitempty"` 24 | GivenName string `json:"given_name,omitempty"` 25 | FamilyName string `json:"family_name,omitempty"` 26 | MiddleName string `json:"middle_name,omitempty"` 27 | Name string `json:"name,omitempty"` 28 | AlternativeID string `json:"alternative_account_id,omitempty"` 29 | RawClientInfo string `json:"client_info,omitempty"` 30 | UserAssertionHash string `json:"user_assertion_hash,omitempty"` 31 | 32 | AdditionalFields map[string]interface{} 33 | } 34 | 35 | // NewAccount creates an account. 36 | func NewAccount(homeAccountID, env, realm, localAccountID, authorityType, username string) Account { 37 | return Account{ 38 | HomeAccountID: homeAccountID, 39 | Environment: env, 40 | Realm: realm, 41 | LocalAccountID: localAccountID, 42 | AuthorityType: authorityType, 43 | PreferredUsername: username, 44 | } 45 | } 46 | 47 | // Key creates the key for storing accounts in the cache. 48 | func (acc Account) Key() string { 49 | key := strings.Join([]string{acc.HomeAccountID, acc.Environment, acc.Realm}, CacheKeySeparator) 50 | return strings.ToLower(key) 51 | } 52 | 53 | // IsZero checks the zero value of account. 54 | func (acc Account) IsZero() bool { 55 | v := reflect.ValueOf(acc) 56 | for i := 0; i < v.NumField(); i++ { 57 | field := v.Field(i) 58 | if !field.IsZero() { 59 | switch field.Kind() { 60 | case reflect.Map, reflect.Slice: 61 | if field.Len() == 0 { 62 | continue 63 | } 64 | } 65 | return false 66 | } 67 | } 68 | return true 69 | } 70 | 71 | // DefaultClient is our default shared HTTP client. 72 | var DefaultClient = &http.Client{} 73 | -------------------------------------------------------------------------------- /apps/internal/shared/shared_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | package shared 5 | 6 | import ( 7 | stdJSON "encoding/json" 8 | "testing" 9 | 10 | "github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/json" 11 | 12 | "github.com/kylelemons/godebug/pretty" 13 | ) 14 | 15 | var ( 16 | accHID = "hid" 17 | accEnv = "env" 18 | accRealm = "realm" 19 | authType = "MSSTS" 20 | accLid = "lid" 21 | accUser = "user" 22 | ) 23 | 24 | func TestAccountUnmarshal(t *testing.T) { 25 | jsonMap := map[string]interface{}{ 26 | "home_account_id": "hid", 27 | "environment": "env", 28 | "extra": "this_is_extra", 29 | "authority_type": authType, 30 | } 31 | 32 | b, err := stdJSON.Marshal(jsonMap) 33 | if err != nil { 34 | panic(err) 35 | } 36 | 37 | want := Account{ 38 | HomeAccountID: accHID, 39 | Environment: accEnv, 40 | AuthorityType: authType, 41 | AdditionalFields: map[string]interface{}{ 42 | "extra": json.MarshalRaw("this_is_extra"), 43 | }, 44 | } 45 | 46 | got := Account{} 47 | err = json.Unmarshal(b, &got) 48 | if err != nil { 49 | panic(err) 50 | } 51 | 52 | if diff := pretty.Compare(want, got); diff != "" { 53 | t.Errorf("TestAccountUnmarshal: -want/+got:\n%s", diff) 54 | } 55 | } 56 | 57 | func TestAccountKey(t *testing.T) { 58 | acc := &Account{ 59 | HomeAccountID: accHID, 60 | Environment: accEnv, 61 | Realm: accRealm, 62 | } 63 | expectedKey := "hid-env-realm" 64 | actualKey := acc.Key() 65 | if expectedKey != actualKey { 66 | t.Errorf("Actual key %s differs from expected key %s", actualKey, expectedKey) 67 | } 68 | } 69 | 70 | func TestAccountMarshal(t *testing.T) { 71 | acc := Account{ 72 | HomeAccountID: accHID, 73 | Environment: accEnv, 74 | Realm: accRealm, 75 | LocalAccountID: accLid, 76 | AuthorityType: authType, 77 | PreferredUsername: accUser, 78 | AdditionalFields: map[string]interface{}{"extra": "extra"}, 79 | } 80 | 81 | want := map[string]interface{}{ 82 | "home_account_id": "hid", 83 | "environment": "env", 84 | "realm": "realm", 85 | "local_account_id": "lid", 86 | "authority_type": authType, 87 | "username": "user", 88 | "extra": "extra", 89 | } 90 | b, err := json.Marshal(acc) 91 | if err != nil { 92 | panic(err) 93 | } 94 | 95 | got := map[string]interface{}{} 96 | if err := stdJSON.Unmarshal(b, &got); err != nil { 97 | panic(err) 98 | } 99 | 100 | if diff := pretty.Compare(want, got); diff != "" { 101 | t.Errorf("TestAccountMarshal: -want/+got:\n%s", diff) 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /apps/internal/version/version.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | // Package version keeps the version number of the client package. 5 | package version 6 | 7 | // Version is the version of this client package that is communicated to the server. 8 | const Version = "1.4.2" 9 | -------------------------------------------------------------------------------- /apps/managedidentity/azure_ml.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | package managedidentity 5 | 6 | import ( 7 | "context" 8 | "net/http" 9 | "os" 10 | ) 11 | 12 | func createAzureMLAuthRequest(ctx context.Context, id ID, resource string) (*http.Request, error) { 13 | req, err := http.NewRequestWithContext(ctx, http.MethodGet, os.Getenv(msiEndpointEnvVar), nil) 14 | if err != nil { 15 | return nil, err 16 | } 17 | 18 | req.Header.Set("secret", os.Getenv(msiSecretEnvVar)) 19 | q := req.URL.Query() 20 | q.Set(apiVersionQueryParameterName, azureMLAPIVersion) 21 | q.Set(resourceQueryParameterName, resource) 22 | q.Set("clientid", os.Getenv("DEFAULT_IDENTITY_CLIENT_ID")) 23 | if cid, ok := id.(UserAssignedClientID); ok { 24 | q.Set("clientid", string(cid)) 25 | } 26 | req.URL.RawQuery = q.Encode() 27 | return req, nil 28 | } 29 | -------------------------------------------------------------------------------- /apps/managedidentity/cloud_shell.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | package managedidentity 5 | 6 | import ( 7 | "context" 8 | "fmt" 9 | "io" 10 | "net/http" 11 | "net/url" 12 | "os" 13 | "strings" 14 | ) 15 | 16 | func createCloudShellAuthRequest(ctx context.Context, resource string) (*http.Request, error) { 17 | msiEndpoint := os.Getenv(msiEndpointEnvVar) 18 | msiEndpointParsed, err := url.Parse(msiEndpoint) 19 | if err != nil { 20 | return nil, fmt.Errorf("couldn't parse %q: %s", msiEndpoint, err) 21 | } 22 | 23 | data := url.Values{} 24 | data.Set(resourceQueryParameterName, resource) 25 | msiDataEncoded := data.Encode() 26 | body := io.NopCloser(strings.NewReader(msiDataEncoded)) 27 | 28 | req, err := http.NewRequestWithContext(ctx, http.MethodPost, msiEndpointParsed.String(), body) 29 | if err != nil { 30 | return nil, fmt.Errorf("error creating http request %s", err) 31 | } 32 | 33 | req.Header.Set(metaHTTPHeaderName, "true") 34 | req.Header.Set("Content-Type", "application/x-www-form-urlencoded") 35 | 36 | return req, nil 37 | } 38 | -------------------------------------------------------------------------------- /apps/managedidentity/servicefabric.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | package managedidentity 5 | 6 | import ( 7 | "context" 8 | "net/http" 9 | "os" 10 | ) 11 | 12 | func createServiceFabricAuthRequest(ctx context.Context, resource string) (*http.Request, error) { 13 | identityEndpoint := os.Getenv(identityEndpointEnvVar) 14 | req, err := http.NewRequestWithContext(ctx, http.MethodGet, identityEndpoint, nil) 15 | if err != nil { 16 | return nil, err 17 | } 18 | req.Header.Set("Accept", "application/json") 19 | req.Header.Set("Secret", os.Getenv(identityHeaderEnvVar)) 20 | q := req.URL.Query() 21 | q.Set("api-version", serviceFabricAPIVersion) 22 | q.Set("resource", resource) 23 | req.URL.RawQuery = q.Encode() 24 | return req, nil 25 | } 26 | -------------------------------------------------------------------------------- /apps/managedidentity/servicefabric_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | package managedidentity 5 | 6 | import ( 7 | "context" 8 | "net/http" 9 | "net/url" 10 | "strings" 11 | "testing" 12 | 13 | "github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/base" 14 | "github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/base/storage" 15 | "github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/mock" 16 | ) 17 | 18 | func TestServiceFabricAcquireTokenReturnsTokenSuccess(t *testing.T) { 19 | setEnvVars(t, ServiceFabric) 20 | testCases := []struct { 21 | resource string 22 | miType ID 23 | }{ 24 | {resource: resource, miType: SystemAssigned()}, 25 | {resource: resourceDefaultSuffix, miType: SystemAssigned()}, 26 | } 27 | for _, testCase := range testCases { 28 | t.Run(string(DefaultToIMDS)+"-"+testCase.miType.value(), func(t *testing.T) { 29 | endpoint := imdsDefaultEndpoint 30 | var localUrl *url.URL 31 | var localHeader http.Header 32 | mockClient := mock.NewClient() 33 | responseBody, err := getSuccessfulResponse(resource, true) 34 | if err != nil { 35 | t.Fatalf(errorFormingJsonResponse, err.Error()) 36 | } 37 | 38 | mockClient.AppendResponse(mock.WithHTTPStatusCode(http.StatusOK), mock.WithBody(responseBody), mock.WithCallback(func(r *http.Request) { 39 | localUrl = r.URL 40 | localHeader = r.Header 41 | })) 42 | // resetting cache 43 | before := cacheManager 44 | defer func() { cacheManager = before }() 45 | cacheManager = storage.New(nil) 46 | 47 | client, err := New(testCase.miType, WithHTTPClient(mockClient)) 48 | if err != nil { 49 | t.Fatal(err) 50 | } 51 | result, err := client.AcquireToken(context.Background(), testCase.resource) 52 | if err != nil { 53 | t.Fatal(err) 54 | } 55 | if localUrl == nil || !strings.HasPrefix(localUrl.String(), "http://localhost:40342/metadata/identity/oauth2/token") { 56 | t.Fatalf("url request is not on %s got %s", endpoint, localUrl) 57 | } 58 | query := localUrl.Query() 59 | 60 | if got := query.Get(apiVersionQueryParameterName); got != serviceFabricAPIVersion { 61 | t.Fatalf("api-version not on %s got %s", serviceFabricAPIVersion, got) 62 | } 63 | if query.Get(resourceQueryParameterName) != strings.TrimSuffix(testCase.resource, "/.default") { 64 | t.Fatal("suffix /.default was not removed.") 65 | } 66 | if localHeader.Get("Accept") != "application/json" { 67 | t.Fatalf("expected Accept header to be application/json, got %s", localHeader.Get("Accept")) 68 | } 69 | if localHeader.Get("Secret") != "secret" { 70 | t.Fatalf("expected secret to be secret, got %s", query.Get("Secret")) 71 | } 72 | if result.Metadata.TokenSource != base.TokenSourceIdentityProvider { 73 | t.Fatalf("expected IndenityProvider tokensource, got %d", result.Metadata.TokenSource) 74 | } 75 | if result.AccessToken != token { 76 | t.Fatalf("wanted %q, got %q", token, result.AccessToken) 77 | } 78 | result, err = client.AcquireToken(context.Background(), testCase.resource) 79 | if err != nil { 80 | t.Fatal(err) 81 | } 82 | if result.Metadata.TokenSource != base.TokenSourceCache { 83 | t.Fatalf("wanted cache token source, got %d", result.Metadata.TokenSource) 84 | } 85 | secondFakeClient, err := New(testCase.miType, WithHTTPClient(mockClient)) 86 | if err != nil { 87 | t.Fatal(err) 88 | } 89 | result, err = secondFakeClient.AcquireToken(context.Background(), testCase.resource) 90 | if err != nil { 91 | t.Fatal(err) 92 | } 93 | if result.Metadata.TokenSource != base.TokenSourceCache { 94 | t.Fatalf("cache result wanted cache token source, got %d", result.Metadata.TokenSource) 95 | } 96 | }) 97 | } 98 | } 99 | func TestServiceFabricErrors(t *testing.T) { 100 | setEnvVars(t, ServiceFabric) 101 | mockClient := mock.NewClient() 102 | 103 | for _, testCase := range []ID{ 104 | UserAssignedObjectID("ObjectId"), 105 | UserAssignedResourceID("resourceid"), 106 | UserAssignedClientID("ClientID")} { 107 | _, err := New(testCase, WithHTTPClient(mockClient)) 108 | if err == nil { 109 | t.Fatal("expected error: Service Fabric API doesn't support specifying a user-assigned identity. The identity is determined by cluster resource configuration. See https://aka.ms/servicefabricmi") 110 | } 111 | if err.Error() != "Service Fabric API doesn't support specifying a user-assigned identity. The identity is determined by cluster resource configuration. See https://aka.ms/servicefabricmi" { 112 | t.Fatalf("expected error: Service Fabric API doesn't support specifying a user-assigned identity. The identity is determined by cluster resource configuration. See https://aka.ms/servicefabricmi, got error: %q", err) 113 | } 114 | 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /apps/public/example_test.go: -------------------------------------------------------------------------------- 1 | package public_test 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/AzureAD/microsoft-authentication-library-for-go/apps/public" 7 | ) 8 | 9 | // This example demonstrates the general pattern for authenticating with MSAL Go: 10 | // - create a client (only necessary at application start--it's best to reuse client instances) 11 | // - call AcquireTokenSilent() to search for a cached access token 12 | // - if the cache misses, acquire a new token 13 | func Example() { 14 | client, err := public.New("client_id", public.WithAuthority("https://login.microsoftonline.com/your_tenant")) 15 | if err != nil { 16 | // TODO: handle error 17 | } 18 | 19 | var result public.AuthResult 20 | scopes := []string{"scope"} 21 | 22 | // If your application previously authenticated a user, call AcquireTokenSilent with that user's account 23 | // to use cached authentication data. This example shows choosing an account from the cache, however this 24 | // isn't always necessary because the AuthResult returned by authentication methods includes user account 25 | // information. 26 | accounts, err := client.Accounts(context.TODO()) 27 | if err != nil { 28 | // TODO: handle error 29 | } 30 | if len(accounts) > 0 { 31 | // There may be more accounts; here we assume the first one is wanted. 32 | // AcquireTokenSilent returns a non-nil error when it can't provide a token. 33 | result, err = client.AcquireTokenSilent(context.TODO(), scopes, public.WithSilentAccount(accounts[0])) 34 | } 35 | if err != nil || len(accounts) == 0 { 36 | // cache miss, authenticate a user with another AcquireToken* method 37 | result, err = client.AcquireTokenInteractive(context.TODO(), scopes) 38 | if err != nil { 39 | // TODO: handle error 40 | } 41 | } 42 | 43 | // TODO: save the authenticated user's account, use the access token 44 | _ = result.Account 45 | _ = result.AccessToken 46 | } 47 | -------------------------------------------------------------------------------- /apps/testdata/test-cert-chain-reverse.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIFGTCCAwGgAwIBAgIUBpOlpNN/cgasvozVw6mfa04+ZC0wDQYJKoZIhvcNAQEL 3 | BQAwOzELMAkGA1UEBhMCVVMxDDAKBgNVBAoMA3h6eTEMMAoGA1UECwwDYWJjMRAw 4 | DgYDVQQDDAdST09ULUNOMCAXDTIwMDgyMTE3MTAyNVoYDzMzODkwODA0MTcxMDI1 5 | WjA+MQswCQYDVQQGEwJVUzEMMAoGA1UECgwDeHl6MQwwCgYDVQQLDANhYmMxEzAR 6 | BgNVBAMMCklOVEVSSU0tQ04wggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoIC 7 | AQCr+Tblr4DhX3Xahbei00OJnUgRw6FMsnyROZ170Lx0YNcOrRJ9PuaOZiYXY2Hm 8 | t71o/PZjMtmiYMIxFaiMnql/dCca777l+uBmlwFOR8bquBWiLStmPpvf7Kh5GZNw 9 | XvLGAhk/oxG0O9Pa3OfrlD5vrn/UEGJBu0C+c6ZSLyRk8RjAh8ZbUvnDhhQw3PoK 10 | MQSmFK8BN8X34elu7kq0j7nS0D6Mt7eS40oYeHEaQDdBGl8f7rcqC3RjJ/b/F9wA 11 | +CsKaps6TvpxE7ln9Y3+0yscgeRbyHW0zem6U7MMvVnK/znuNY90Wmajbea7SUj6 12 | nGZpLGS1TqS4H5rn9U1N1WCSyFukTpAQLCPQHeUrSiHKa9Ye5KuC6u2ZXgy0qpGj 13 | nMLu+7746wemi7jN06yZjEmDVneMNCxjLYs4ZhuhiTEItlZpR0VBugNbKo2mJw2U 14 | UesizB3AzQkqGOKp70y74yC+ykLkR5vRNyY3MENJ+W83U1haS7C1rhqFV4eXflVe 15 | EHl8tj7p4KrfhSPr0Rd12UIWDXkYUpCAPlDMdEa9+SDAyuSnkN4P1fAeuzG01jeJ 16 | bnsrWgs3gH3KaGBcPTV4tOTavilGNYDvHZbN9XpYZoZQoPrDZc61M5Ol/cxBahkO 17 | n4aDyhpx5hHnSs7VQuHnjeMUxt3J5HqrXPvaf6uPYNT8KQIDAQABoxAwDjAMBgNV 18 | HRMEBTADAQH/MA0GCSqGSIb3DQEBCwUAA4ICAQCHCxFqJwfVMI9kMvwlj+sxd4Q5 19 | KuyWxlXRfzpYZ/6JCUq7VBceRVJ87KytMNCyq61rd3Jhb8ssoMCENB68HYhIFUGz 20 | GR92AAc6LTh2Y3vQAg640Cz2vLCGnqnlbIslYV6fzxYqgSopR5wJ4D/kJ9w7NSrC 21 | paN6bS8Olv//tN6RSnvEMJZdXFA40xFin6qT8Op3nrysEE7Z84wPG9Wj2DXskX6v 22 | bZenCEgl1/Ezif5IEgJcYdRkXtYPp6JNbVV+KjDTIMEaUVMpGMGefrt22E+4nSa3 23 | qFvcbzYEKeANe9IAxdPzeWiQ2U90PqWFYCA9sOVsrlSwrup+yYXl0yhTxKY67NCX 24 | gyVtZRnzawv0AVFsfCOT4V0wJSuUz4BV6sH7kl2C7FW3zqYVdFEDigbUNsEEh/jF 25 | 3JiAtgNbpJ8TtiCFrCI4g9Jepa3polVPzDD8mLtkWWnfSBN/28cxa2jiUlfQxB39 26 | kyqu4rWbm01lyucJxVgJzH0SGyEM5OvF/OIOU3Q7UIXEcZSX3m4Xo59+v6ZNDwKL 27 | PcFDNK+PL3WNYfdexQCSAbLm1gkUrVIqvidpCSSVv5oWwTM5m7rbA16Hlu4Ea2ep 28 | Pl7I9YXXXnIEFqLYZDnCJglcXmlt6OjI8D3w0TRWHb6bFqubDP417sJDX1S6udN5 29 | wOnOIqg0ZZcqfvpxXA== 30 | -----END CERTIFICATE----- 31 | -----BEGIN CERTIFICATE----- 32 | MIID7zCCAdcCAQEwDQYJKoZIhvcNAQEFBQAwPjELMAkGA1UEBhMCVVMxDDAKBgNV 33 | BAoMA3h5ejEMMAoGA1UECwwDYWJjMRMwEQYDVQQDDApJTlRFUklNLUNOMCAXDTIw 34 | MDgyMTE3MTA0M1oYDzMzODkwODA0MTcxMDQzWjA7MQswCQYDVQQGEwJVUzEMMAoG 35 | A1UECgwDeHl6MQwwCgYDVQQLDANhYmMxEDAOBgNVBAMMB1VTRVItQ04wggEiMA0G 36 | CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC6eQYdbIFhsinob3t3AV4yEH/tz/LV 37 | I+UAGLpxQnqGnuAV5GY3CXiAO8GZjx7y3oA1DGfe+/cc6n9BmYWXsKvxpKO8PQkB 38 | PYIFtD878uDNv7kVoZG8EVsEngBxd4efMniKWwKtMle0hZ+jj3u4Ad49DsXcC0L2 39 | 8uV/eQ6hzsQiR0nTQJ/4QqNNtThSGAFSr7Oo8xzxBNTJhe+BvwDE8JMkCS0v22JW 40 | my2GYrRKw4RlSKxwv9QZr83gSicKSUPUACBYfJ7RuXSQOHOMlIcC4oGtDrMshGzr 41 | 704Ho+DiByYf5G6nkfZ1I7T039gEKKIllNKWqhyQHejKba3nP163ZKI3AgMBAAEw 42 | DQYJKoZIhvcNAQEFBQADggIBADfitSfjlYa2inBKlpWN8VT0DPm5uw8EHuwLymCM 43 | WYrQMCuQVE2xYoqCSmXj6KLFt8ycgxHsthdkAzXxDhawaKjz2UFp6nszmUA4xfvS 44 | mxLSajwzK/KMBkjdFL7TM+TTBJ1bleDbmoJvDiUeQwisbb1Uh8b3v/jpBwoiamm8 45 | Y4Ca5A15SeBUvAt0/Mc4XJfZ/Ts+LBAPevI9ZyU7C5JZky1q41KPklEHfFZKQRfP 46 | cTyTYYvlPoq57C8XPDs6r50EV3B6Z8MN21OB6MVGi8BOY/c7a2h1ZOhxNyBnJuQX 47 | w4meJthoKcHUnAs8YCrEoQKayMqPH0Vdhaii/gx4jAgh4PNyIZz5cAst+ybPtQj4 48 | i7LFEWjxis+NLQMHhyE4fIGIkEjzU0uGDugifheIwKALqYEgMDrcoolwvGMdPxGo 49 | Qps7tkad5vZV9d9+tTbI+DMB16Y51S04/u1dGFz3jSrDVF08PznJc99VB69OReiC 50 | K17n8Xyox/VAaYsRFbOAJpLRWwcnotDpFQbgiLrmXxNOoiWPNbQsQzaQx7cR9okQ 51 | v5RTpFAkrdjadhMsXFFiQh+axlaGD368ZGAj5ZoyOiXkV88tNCtyP/RDgW5ftQQ7 52 | fdv05bNXhDfLgEgQvVSDfClDL1hKukLmLQS3ILfB4FlM/XmE+FW/qgo9aSx2XIbx 53 | E4ie 54 | -----END CERTIFICATE----- 55 | -----BEGIN RSA PRIVATE KEY----- 56 | MIIEowIBAAKCAQEAunkGHWyBYbIp6G97dwFeMhB/7c/y1SPlABi6cUJ6hp7gFeRm 57 | Nwl4gDvBmY8e8t6ANQxn3vv3HOp/QZmFl7Cr8aSjvD0JAT2CBbQ/O/Lgzb+5FaGR 58 | vBFbBJ4AcXeHnzJ4ilsCrTJXtIWfo497uAHePQ7F3AtC9vLlf3kOoc7EIkdJ00Cf 59 | +EKjTbU4UhgBUq+zqPMc8QTUyYXvgb8AxPCTJAktL9tiVpsthmK0SsOEZUiscL/U 60 | Ga/N4EonCklD1AAgWHye0bl0kDhzjJSHAuKBrQ6zLIRs6+9OB6Pg4gcmH+Rup5H2 61 | dSO09N/YBCiiJZTSlqockB3oym2t5z9et2SiNwIDAQABAoIBAQCKzivPG0X0AztO 62 | 2i19mHcVrVKNI44POnjsaXvfcyzhqMIFic7MiTA5xEGInRDcmOO2mVV4lvaLf8La 63 | gfz/vXNAnN2E8aoSUkbHGDU52sGcZmrPv0VMSV8HQNXzoJZD2r3/v19urVq79fuv 64 | NM9TWZCkwqpl8bwXNxe+m85YhCFboY9G543qmuXzKAQLoSupT0e4eIo2IGp7eJYK 65 | 5J/wtlEumUdhsKo1ajLojDgsgPKfrCyvsmO+bj1dRKGXVLO2SL2pFVCjjHF4SP3q 66 | 1WX39beu61Zu+kGthDgj5muHgH06FtnWoHLIUrRmYpM+ezCxQHdRWz7AYjheeE7q 67 | QqJv1PqBAoGBAOlb/gzsps+rInE+LQoEzVj8osILI4NxIpNc6+iG81dEi+zQABX/ 68 | bHV6hXGGceozVcX4B+V7f08PlZIAgM3IDqfy0fH2pwEQahJ8a3MwzCgR66RxYlkX 69 | E8czkoz0pcHW58FnLLlWXpHRALTtqoPP5LnWs0SmoNvcHZ9yjJ6tvpRlAoGBAMyQ 70 | fytsyla1ujO0l/kuLFG7gndeOc96SutH3V17lZ1pN0efHyk2aglOnl6YsdPKLZvZ 71 | 3ghj01HV0Q0f//xpftduuA7gdgDzSG1irXsxEidfVxX7RsPxX6cx8dhYnuk5rz5E 72 | XyTko7zTpr+A4XMnq6+JNSSCIE+CVYcYf/hyemxrAoGAeC9py4xCaWgxR/OGzMcm 73 | X3NV++wysSqebRkJYuvF/icOjbuen7W6TVL50Ts2BjHENj6FCpqtObHEDbr2m4Uy 74 | jysPF7g50OF8T+MGkAAM1YJNQ5cl2M564DhefPwvNoMRP1l8/kNOV3k2DPjuvg5f 75 | NZsvHudWp4VZOFqNs9e19MUCgYAjewCDoKfrqDN2mmEtmAOZ3YMAfzhZsyVhb6KG 76 | f1Pw7HnpE0FNXaHAoYE4eRWG3W9Rs9Ud8WqKrCJJO36j4gxdA1grRGVTPt8WEeJz 77 | FozGhXPOXTnl7GyhzDjdRGmznAy4KRWziXCY5MDsQEdaOMw/cvXjsio2gC2jc+1m 78 | QzzWpwKBgHzszJ5s6vcWElox4Yc1elQ8xniPpo3RtfXZOLX8xA4eR9yQawah1zd6 79 | ChfeYbHVfq007s+RWGTb+KYQ6ic9nkW464qmVxHGBatUo9+MR4Gk8blANoAfHxdV 80 | g6JNgT2kIGu9IEwoD6XQldC/v24bvFSesyGRHNdI4mUG+hhU4aNw 81 | -----END RSA PRIVATE KEY----- 82 | -------------------------------------------------------------------------------- /apps/testdata/test-cert-chain.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEowIBAAKCAQEAunkGHWyBYbIp6G97dwFeMhB/7c/y1SPlABi6cUJ6hp7gFeRm 3 | Nwl4gDvBmY8e8t6ANQxn3vv3HOp/QZmFl7Cr8aSjvD0JAT2CBbQ/O/Lgzb+5FaGR 4 | vBFbBJ4AcXeHnzJ4ilsCrTJXtIWfo497uAHePQ7F3AtC9vLlf3kOoc7EIkdJ00Cf 5 | +EKjTbU4UhgBUq+zqPMc8QTUyYXvgb8AxPCTJAktL9tiVpsthmK0SsOEZUiscL/U 6 | Ga/N4EonCklD1AAgWHye0bl0kDhzjJSHAuKBrQ6zLIRs6+9OB6Pg4gcmH+Rup5H2 7 | dSO09N/YBCiiJZTSlqockB3oym2t5z9et2SiNwIDAQABAoIBAQCKzivPG0X0AztO 8 | 2i19mHcVrVKNI44POnjsaXvfcyzhqMIFic7MiTA5xEGInRDcmOO2mVV4lvaLf8La 9 | gfz/vXNAnN2E8aoSUkbHGDU52sGcZmrPv0VMSV8HQNXzoJZD2r3/v19urVq79fuv 10 | NM9TWZCkwqpl8bwXNxe+m85YhCFboY9G543qmuXzKAQLoSupT0e4eIo2IGp7eJYK 11 | 5J/wtlEumUdhsKo1ajLojDgsgPKfrCyvsmO+bj1dRKGXVLO2SL2pFVCjjHF4SP3q 12 | 1WX39beu61Zu+kGthDgj5muHgH06FtnWoHLIUrRmYpM+ezCxQHdRWz7AYjheeE7q 13 | QqJv1PqBAoGBAOlb/gzsps+rInE+LQoEzVj8osILI4NxIpNc6+iG81dEi+zQABX/ 14 | bHV6hXGGceozVcX4B+V7f08PlZIAgM3IDqfy0fH2pwEQahJ8a3MwzCgR66RxYlkX 15 | E8czkoz0pcHW58FnLLlWXpHRALTtqoPP5LnWs0SmoNvcHZ9yjJ6tvpRlAoGBAMyQ 16 | fytsyla1ujO0l/kuLFG7gndeOc96SutH3V17lZ1pN0efHyk2aglOnl6YsdPKLZvZ 17 | 3ghj01HV0Q0f//xpftduuA7gdgDzSG1irXsxEidfVxX7RsPxX6cx8dhYnuk5rz5E 18 | XyTko7zTpr+A4XMnq6+JNSSCIE+CVYcYf/hyemxrAoGAeC9py4xCaWgxR/OGzMcm 19 | X3NV++wysSqebRkJYuvF/icOjbuen7W6TVL50Ts2BjHENj6FCpqtObHEDbr2m4Uy 20 | jysPF7g50OF8T+MGkAAM1YJNQ5cl2M564DhefPwvNoMRP1l8/kNOV3k2DPjuvg5f 21 | NZsvHudWp4VZOFqNs9e19MUCgYAjewCDoKfrqDN2mmEtmAOZ3YMAfzhZsyVhb6KG 22 | f1Pw7HnpE0FNXaHAoYE4eRWG3W9Rs9Ud8WqKrCJJO36j4gxdA1grRGVTPt8WEeJz 23 | FozGhXPOXTnl7GyhzDjdRGmznAy4KRWziXCY5MDsQEdaOMw/cvXjsio2gC2jc+1m 24 | QzzWpwKBgHzszJ5s6vcWElox4Yc1elQ8xniPpo3RtfXZOLX8xA4eR9yQawah1zd6 25 | ChfeYbHVfq007s+RWGTb+KYQ6ic9nkW464qmVxHGBatUo9+MR4Gk8blANoAfHxdV 26 | g6JNgT2kIGu9IEwoD6XQldC/v24bvFSesyGRHNdI4mUG+hhU4aNw 27 | -----END RSA PRIVATE KEY----- 28 | -----BEGIN CERTIFICATE----- 29 | MIID7zCCAdcCAQEwDQYJKoZIhvcNAQEFBQAwPjELMAkGA1UEBhMCVVMxDDAKBgNV 30 | BAoMA3h5ejEMMAoGA1UECwwDYWJjMRMwEQYDVQQDDApJTlRFUklNLUNOMCAXDTIw 31 | MDgyMTE3MTA0M1oYDzMzODkwODA0MTcxMDQzWjA7MQswCQYDVQQGEwJVUzEMMAoG 32 | A1UECgwDeHl6MQwwCgYDVQQLDANhYmMxEDAOBgNVBAMMB1VTRVItQ04wggEiMA0G 33 | CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC6eQYdbIFhsinob3t3AV4yEH/tz/LV 34 | I+UAGLpxQnqGnuAV5GY3CXiAO8GZjx7y3oA1DGfe+/cc6n9BmYWXsKvxpKO8PQkB 35 | PYIFtD878uDNv7kVoZG8EVsEngBxd4efMniKWwKtMle0hZ+jj3u4Ad49DsXcC0L2 36 | 8uV/eQ6hzsQiR0nTQJ/4QqNNtThSGAFSr7Oo8xzxBNTJhe+BvwDE8JMkCS0v22JW 37 | my2GYrRKw4RlSKxwv9QZr83gSicKSUPUACBYfJ7RuXSQOHOMlIcC4oGtDrMshGzr 38 | 704Ho+DiByYf5G6nkfZ1I7T039gEKKIllNKWqhyQHejKba3nP163ZKI3AgMBAAEw 39 | DQYJKoZIhvcNAQEFBQADggIBADfitSfjlYa2inBKlpWN8VT0DPm5uw8EHuwLymCM 40 | WYrQMCuQVE2xYoqCSmXj6KLFt8ycgxHsthdkAzXxDhawaKjz2UFp6nszmUA4xfvS 41 | mxLSajwzK/KMBkjdFL7TM+TTBJ1bleDbmoJvDiUeQwisbb1Uh8b3v/jpBwoiamm8 42 | Y4Ca5A15SeBUvAt0/Mc4XJfZ/Ts+LBAPevI9ZyU7C5JZky1q41KPklEHfFZKQRfP 43 | cTyTYYvlPoq57C8XPDs6r50EV3B6Z8MN21OB6MVGi8BOY/c7a2h1ZOhxNyBnJuQX 44 | w4meJthoKcHUnAs8YCrEoQKayMqPH0Vdhaii/gx4jAgh4PNyIZz5cAst+ybPtQj4 45 | i7LFEWjxis+NLQMHhyE4fIGIkEjzU0uGDugifheIwKALqYEgMDrcoolwvGMdPxGo 46 | Qps7tkad5vZV9d9+tTbI+DMB16Y51S04/u1dGFz3jSrDVF08PznJc99VB69OReiC 47 | K17n8Xyox/VAaYsRFbOAJpLRWwcnotDpFQbgiLrmXxNOoiWPNbQsQzaQx7cR9okQ 48 | v5RTpFAkrdjadhMsXFFiQh+axlaGD368ZGAj5ZoyOiXkV88tNCtyP/RDgW5ftQQ7 49 | fdv05bNXhDfLgEgQvVSDfClDL1hKukLmLQS3ILfB4FlM/XmE+FW/qgo9aSx2XIbx 50 | E4ie 51 | -----END CERTIFICATE----- 52 | -----BEGIN CERTIFICATE----- 53 | MIIFGTCCAwGgAwIBAgIUBpOlpNN/cgasvozVw6mfa04+ZC0wDQYJKoZIhvcNAQEL 54 | BQAwOzELMAkGA1UEBhMCVVMxDDAKBgNVBAoMA3h6eTEMMAoGA1UECwwDYWJjMRAw 55 | DgYDVQQDDAdST09ULUNOMCAXDTIwMDgyMTE3MTAyNVoYDzMzODkwODA0MTcxMDI1 56 | WjA+MQswCQYDVQQGEwJVUzEMMAoGA1UECgwDeHl6MQwwCgYDVQQLDANhYmMxEzAR 57 | BgNVBAMMCklOVEVSSU0tQ04wggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoIC 58 | AQCr+Tblr4DhX3Xahbei00OJnUgRw6FMsnyROZ170Lx0YNcOrRJ9PuaOZiYXY2Hm 59 | t71o/PZjMtmiYMIxFaiMnql/dCca777l+uBmlwFOR8bquBWiLStmPpvf7Kh5GZNw 60 | XvLGAhk/oxG0O9Pa3OfrlD5vrn/UEGJBu0C+c6ZSLyRk8RjAh8ZbUvnDhhQw3PoK 61 | MQSmFK8BN8X34elu7kq0j7nS0D6Mt7eS40oYeHEaQDdBGl8f7rcqC3RjJ/b/F9wA 62 | +CsKaps6TvpxE7ln9Y3+0yscgeRbyHW0zem6U7MMvVnK/znuNY90Wmajbea7SUj6 63 | nGZpLGS1TqS4H5rn9U1N1WCSyFukTpAQLCPQHeUrSiHKa9Ye5KuC6u2ZXgy0qpGj 64 | nMLu+7746wemi7jN06yZjEmDVneMNCxjLYs4ZhuhiTEItlZpR0VBugNbKo2mJw2U 65 | UesizB3AzQkqGOKp70y74yC+ykLkR5vRNyY3MENJ+W83U1haS7C1rhqFV4eXflVe 66 | EHl8tj7p4KrfhSPr0Rd12UIWDXkYUpCAPlDMdEa9+SDAyuSnkN4P1fAeuzG01jeJ 67 | bnsrWgs3gH3KaGBcPTV4tOTavilGNYDvHZbN9XpYZoZQoPrDZc61M5Ol/cxBahkO 68 | n4aDyhpx5hHnSs7VQuHnjeMUxt3J5HqrXPvaf6uPYNT8KQIDAQABoxAwDjAMBgNV 69 | HRMEBTADAQH/MA0GCSqGSIb3DQEBCwUAA4ICAQCHCxFqJwfVMI9kMvwlj+sxd4Q5 70 | KuyWxlXRfzpYZ/6JCUq7VBceRVJ87KytMNCyq61rd3Jhb8ssoMCENB68HYhIFUGz 71 | GR92AAc6LTh2Y3vQAg640Cz2vLCGnqnlbIslYV6fzxYqgSopR5wJ4D/kJ9w7NSrC 72 | paN6bS8Olv//tN6RSnvEMJZdXFA40xFin6qT8Op3nrysEE7Z84wPG9Wj2DXskX6v 73 | bZenCEgl1/Ezif5IEgJcYdRkXtYPp6JNbVV+KjDTIMEaUVMpGMGefrt22E+4nSa3 74 | qFvcbzYEKeANe9IAxdPzeWiQ2U90PqWFYCA9sOVsrlSwrup+yYXl0yhTxKY67NCX 75 | gyVtZRnzawv0AVFsfCOT4V0wJSuUz4BV6sH7kl2C7FW3zqYVdFEDigbUNsEEh/jF 76 | 3JiAtgNbpJ8TtiCFrCI4g9Jepa3polVPzDD8mLtkWWnfSBN/28cxa2jiUlfQxB39 77 | kyqu4rWbm01lyucJxVgJzH0SGyEM5OvF/OIOU3Q7UIXEcZSX3m4Xo59+v6ZNDwKL 78 | PcFDNK+PL3WNYfdexQCSAbLm1gkUrVIqvidpCSSVv5oWwTM5m7rbA16Hlu4Ea2ep 79 | Pl7I9YXXXnIEFqLYZDnCJglcXmlt6OjI8D3w0TRWHb6bFqubDP417sJDX1S6udN5 80 | wOnOIqg0ZZcqfvpxXA== 81 | -----END CERTIFICATE----- 82 | -------------------------------------------------------------------------------- /apps/testdata/test-cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIICljCCAX4CCQDNgteZ+lJH4zANBgkqhkiG9w0BAQsFADANMQswCQYDVQQGEwJ1 3 | czAeFw0yMTAxMDQyMzQzNDVaFw0yMTAyMDMyMzQzNDVaMA0xCzAJBgNVBAYTAnVz 4 | MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1r58wq7JQxM12viLNbdG 5 | fFizeVQwWRwrx/4CH3kU8jjGovbhkvC/uLWqVGchgATThhGkvNrA92WvdkVwsZMk 6 | Qf7ZnTA7kemo4VFtgo5XCGEej9gOTW13Evdc/0Flip+RXl3h3Q6BbbB9IFE0c6cS 7 | 3i/v/t8KGpVYQHQzBwTcYehM6eDO8ZjUyUUcJOMXdMCctamig7fMGlziKFahn4dX 8 | JoiiK4oNKE9okXIAXCTbVkAxxH0hD+5XH1nn5LJnHe0e5DflI3YIiPgmRL5uC89K 9 | XqmYCKWrq5z2D5k+5fQLmbOcxErBcFCh8hA+Xu0RLT4BHPEgc6iVIqxL4CZi/cke 10 | uwIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQAAyDbm0Fda0/vY6ZVDML2IbGWbro1w 11 | nWYNw6wclNU6sx1oeG/k/y2ni7NImPpbFN+594WS6rYHgFdROfeuNgGnjgQCJogk 12 | +8ouf1R6vFMUAScWeSaFnZmBEgwofWsnIcUKkbDIXbpRhMrkNEcY09VgjmCKhspQ 13 | iX2bJQTj49XBac9tBaJJYDZ4HgkO4nU7QeEPpvwlELZFoZZXtd3fan+VUyFS2a9n 14 | gkAMDYoQPGN4tyGFabWws/GlMxelWvqUzpQKmeRPVz+cij75l8eKThEiu0zbjOTD 15 | Gq81BcY61SPqN02zoPCtqZ/zU6HhaL3x7zUuzhLhNoh83A43UVYEoOOf 16 | -----END CERTIFICATE----- 17 | -----BEGIN PRIVATE KEY----- 18 | MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDWvnzCrslDEzXa 19 | +Is1t0Z8WLN5VDBZHCvH/gIfeRTyOMai9uGS8L+4tapUZyGABNOGEaS82sD3Za92 20 | RXCxkyRB/tmdMDuR6ajhUW2CjlcIYR6P2A5NbXcS91z/QWWKn5FeXeHdDoFtsH0g 21 | UTRzpxLeL+/+3woalVhAdDMHBNxh6Ezp4M7xmNTJRRwk4xd0wJy1qaKDt8waXOIo 22 | VqGfh1cmiKIrig0oT2iRcgBcJNtWQDHEfSEP7lcfWefksmcd7R7kN+UjdgiI+CZE 23 | vm4Lz0peqZgIpaurnPYPmT7l9AuZs5zESsFwUKHyED5e7REtPgEc8SBzqJUirEvg 24 | JmL9yR67AgMBAAECggEAAQ/IBh5fGFnL9l0sMwPI8Wxu1ra31njxLnfvAsDSfbAS 25 | K1QVIWjXSc58HRa1b7CWax9DNTvPoGl8SJVnTTlxAHKGGOTYJoyFLTf91ptlisEQ 26 | KZ3j1DYqVImsiAaGvfyz90d3imQ795Lby4EbRUcaLMcH5LatkhwS556rcelwPXuq 27 | M43XaZu5Es4pG0EmzfXplO/awt5HdUDPEAY3yw7QH8D1/l/toLPyiFv37RezkVK9 28 | ffcUQpH7uH000Gja+JSEHgpWZhE96ac6H0zBtlM1VkMtfBuczz5tkKN/p70fhr8T 29 | ZXARZqIaF4vx7RkBBzCfhvrgGqxXMuvTaW6N4RDWYQKBgQD1iZ7/xr9qy4cPFSOt 30 | yBnG5cE6wC7wP8qgr0N7MgAii5OZgx6rtfGIVJDY58CFijnT8jZ5pjNS3p7j/Rzp 31 | lQJMIwC5kIe/7FU7nmE3ko7Wg+bpd8iWLLIi/QWVFLbS7qVmulTc+CEXWyhAiI2u 32 | RL/1APjIDFKp9gqtKmwb9erxDwKBgQDf5PbGHuPv5RBLJz9du+M/BIBY+HDltG89 33 | p3huHHTjkJ5R38oximf2HnV4ygT/p2+ZUD6TJZZw6qou3/GiU5gZbRpg+4LXtQUR 34 | vV+S2n/t86NG1YcGmM29r8LWqrK9gxLW0X62Fpps16rHSP7kVc4SvmrYwqNzqKlC 35 | D9QbFYYflQKBgQCKEVzrDuNMNi43+PcbHU4BXeiOFMtQJU7XlDYp7C/PPRU+WVDB 36 | 1Yl/062vioHjlZp259hiB2cMzkoigY3kevnTvksGDZOIBGjZIXIhQbQ4Q+twlP6i 37 | E3gH3Kdq8T7s1W0EmvplVtGkxImZ4C9rMxWNu4IpW2SQVd4jCZvJDTuTWQKBgQCn 38 | LGjuCYacSubdlpKDxJSrKwtCY0641P7yhCcx4GGOwR7Vd0mbsAJsDNYduIn+8eAs 39 | E3SFnl00NqOXmHLth4lcAtDddS5/LZR5aHMCTc+TtoVFkI3faRzF84SBkLchNctN 40 | RuNbxojLmETVxDU9/Kt/51oUO1CcPWUUBImVJ38b+QKBgQCTbi0nS0n8kC7nlXWN 41 | QtPcf4UraJAxv1DGq4lnJ8AHSZqqkP5fyjfknSw5ExOPDg4mEHhnnpsvwJuSX00d 42 | UYUN2ZJXPZeaO0HmbYZ3/vC9bo6KW95PhidEUQpGlKrFY342khjQHJtH67YUThwU 43 | lQFhpxvPgPNBuxVRnsxoH/sLOA== 44 | -----END PRIVATE KEY----- 45 | -------------------------------------------------------------------------------- /apps/tests/benchmarks/confidential.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | package main 5 | 6 | import ( 7 | "context" 8 | "fmt" 9 | "os" 10 | "runtime" 11 | "strconv" 12 | "sync" 13 | "text/template" 14 | "time" 15 | 16 | "github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/base" 17 | "github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/oauth" 18 | "github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/oauth/fake" 19 | "github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/oauth/ops/accesstokens" 20 | "github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/oauth/ops/authority" 21 | ) 22 | 23 | const accessToken = "fake_token" 24 | 25 | var tokenScope = []string{"fake_scope"} 26 | 27 | type testParams struct { 28 | // the number of goroutines to use 29 | Concurrency int 30 | 31 | // the number of tokens in the cache 32 | // must be divisible by Concurrency 33 | TokenCount int 34 | } 35 | 36 | func fakeClient() (base.Client, error) { 37 | // we use a base.Client so we can provide a fake OAuth client 38 | return base.New("fake_client_id", "https://fake_authority/fake", &oauth.Client{ 39 | AccessTokens: &fake.AccessTokens{ 40 | AccessToken: accesstokens.TokenResponse{ 41 | AccessToken: accessToken, 42 | ExpiresOn: time.Now().Add(1 * time.Hour), 43 | GrantedScopes: accesstokens.Scopes{Slice: tokenScope}, 44 | }, 45 | }, 46 | Authority: &fake.Authority{ 47 | InstanceResp: authority.InstanceDiscoveryResponse{ 48 | Metadata: []authority.InstanceDiscoveryMetadata{ 49 | { 50 | PreferredNetwork: "fake_authority", 51 | Aliases: []string{"fake_authority"}, 52 | }, 53 | }, 54 | }, 55 | }, 56 | Resolver: &fake.ResolveEndpoints{ 57 | Endpoints: authority.Endpoints{ 58 | AuthorizationEndpoint: "auth_endpoint", 59 | TokenEndpoint: "token_endpoint", 60 | }, 61 | }, 62 | WSTrust: &fake.WSTrust{}, 63 | }) 64 | } 65 | 66 | type execTime struct { 67 | start time.Time 68 | end time.Time 69 | } 70 | 71 | func populateTokenCache(client base.Client, params testParams) execTime { 72 | if r := params.TokenCount % params.Concurrency; r != 0 { 73 | panic("TokenCount must be divisible by Concurrency") 74 | } 75 | parts := params.TokenCount / params.Concurrency 76 | authParams := client.AuthParams 77 | authParams.Scopes = tokenScope 78 | authParams.AuthorizationType = authority.ATClientCredentials 79 | 80 | wg := &sync.WaitGroup{} 81 | fmt.Printf("Populating token cache with %d tokens...", params.TokenCount) 82 | start := time.Now() 83 | for n := 0; n < params.Concurrency; n++ { 84 | wg.Add(1) 85 | go func(chunk int) { 86 | for i := parts * chunk; i < parts*(chunk+1); i++ { 87 | // we use this to add a fake token to the cache. 88 | // each token has a different scope which is what makes them unique 89 | _, err := client.AuthResultFromToken(context.Background(), authParams, accesstokens.TokenResponse{ 90 | AccessToken: accessToken, 91 | ExpiresOn: time.Now().Add(1 * time.Hour), 92 | GrantedScopes: accesstokens.Scopes{Slice: []string{strconv.FormatInt(int64(i), 10)}}, 93 | }) 94 | if err != nil { 95 | panic(err) 96 | } 97 | } 98 | wg.Done() 99 | }(n) 100 | } 101 | wg.Wait() 102 | return execTime{start: start, end: time.Now()} 103 | } 104 | 105 | func executeTest(client base.Client, params testParams) execTime { 106 | wg := &sync.WaitGroup{} 107 | fmt.Printf("Begin token retrieval.....") 108 | start := time.Now() 109 | for n := 0; n < params.Concurrency; n++ { 110 | wg.Add(1) 111 | go func() { 112 | // retrieve each token once per goroutine 113 | for tk := 0; tk < params.TokenCount; tk++ { 114 | _, err := client.AcquireTokenSilent(context.Background(), base.AcquireTokenSilentParameters{ 115 | Scopes: []string{strconv.FormatInt(int64(tk), 10)}, 116 | RequestType: accesstokens.ATConfidential, 117 | Credential: &accesstokens.Credential{ 118 | Secret: "fake_secret", 119 | }, 120 | }) 121 | if err != nil { 122 | panic(err) 123 | } 124 | } 125 | wg.Done() 126 | }() 127 | } 128 | wg.Wait() 129 | return execTime{start: start, end: time.Now()} 130 | } 131 | 132 | // Stats is used with statsTemplText for reporting purposes 133 | type Stats struct { 134 | popExec execTime 135 | retExec execTime 136 | Concurrency int 137 | Count int64 138 | } 139 | 140 | // PopDur returns the total duration for populating the cache. 141 | func (s *Stats) PopDur() time.Duration { 142 | return s.popExec.end.Sub(s.popExec.start) 143 | } 144 | 145 | // RetDur returns the total duration for retrieving tokens. 146 | func (s *Stats) RetDur() time.Duration { 147 | return s.retExec.end.Sub(s.retExec.start) 148 | } 149 | 150 | // PopAvg returns the mean average of caching a token. 151 | func (s *Stats) PopAvg() time.Duration { 152 | return s.PopDur() / time.Duration(s.Count) 153 | } 154 | 155 | // RetAvg returns the mean average of retrieving a token. 156 | func (s *Stats) RetAvg() time.Duration { 157 | return s.RetDur() / time.Duration(s.Count) 158 | } 159 | 160 | var statsTemplText = ` 161 | Test Results: 162 | [{{.Concurrency}} goroutines][{{.Count}} tokens] [population: total {{.PopDur}}, avg {{.PopAvg}}] [retrieval: total {{.RetDur}}, avg {{.RetAvg}}] 163 | ========================================================================== 164 | ` 165 | var statsTempl = template.Must(template.New("stats").Parse(statsTemplText)) 166 | 167 | func main() { 168 | tests := []testParams{ 169 | { 170 | Concurrency: runtime.NumCPU(), 171 | TokenCount: 100, 172 | }, 173 | { 174 | Concurrency: runtime.NumCPU(), 175 | TokenCount: 1000, 176 | }, 177 | { 178 | Concurrency: runtime.NumCPU(), 179 | TokenCount: 10000, 180 | }, 181 | { 182 | Concurrency: runtime.NumCPU(), 183 | TokenCount: 20000, 184 | }, 185 | } 186 | 187 | for _, t := range tests { 188 | client, err := fakeClient() 189 | if err != nil { 190 | panic(err) 191 | } 192 | fmt.Printf("Test Params: %#v\n", t) 193 | ptime := populateTokenCache(client, t) 194 | ttime := executeTest(client, t) 195 | if err := statsTempl.Execute(os.Stdout, &Stats{ 196 | popExec: ptime, 197 | retExec: ttime, 198 | Concurrency: t.Concurrency, 199 | Count: int64(t.TokenCount), 200 | }); err != nil { 201 | panic(err) 202 | } 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /apps/tests/devapps/README.md: -------------------------------------------------------------------------------- 1 | # Running the Dev Apps for MSAL Go 2 | 3 | To run one of the dev app which uses MSAL Go, the `config.json` file and the `confidential_config.json` should look like the following: 4 | 5 | ```json 6 | { 7 | "authority": "https://login.microsoftonline.com/organizations", 8 | "client_id": "your_client_id", 9 | "scopes": ["user.read"], 10 | "username": "your_username", 11 | "password": "your_password", 12 | "redirect_uri": "redirect uri registered on the portal", 13 | "code_challenge": "transformed code verifier from PKCE", 14 | "state": "state parameter for authorization code flow", 15 | "client_secret": "client secret you generated for your app", 16 | "thumbprint": "the certificate thumbprint defined in your app generation", 17 | "pem_file": "the file path of your private key pem" 18 | } 19 | ``` 20 | 21 | The dev apps in this repo get tokens for the MS Graph API. To find permissible scopes for MS Graph, visit this [link](https://docs.microsoft.com/graph/permissions-reference). PKCE is explained [here](https://tools.ietf.org/html/rfc7636#section-4.1). 22 | 23 | ## On Windows 24 | 25 | To run the dev samples: 26 | `cd test/devapps` 27 | 28 | run the command: 29 | 30 | 'go run ./ 1' 31 | 32 | Alternatives: 33 | * 1 build and run "locally" 34 | * In the devapps folder 35 | * type 'go build' 36 | * type 'devapps.exe 1' to run the device code flow 37 | 38 | * 2 (Advanced) install and run from the gobin folder 39 | * See more: https://golang.org/cmd/go/#hdr-Compile_and_install_packages_and_dependencies 40 | * In the devapps folder 41 | * type 'go install' 42 | * locate your gobin folder e.g. type 'go env' to find your gobin folder location 43 | cd to your gobin folder 44 | * type 'devapps.exe 1' to run the device code flow 45 | 46 | ## On Mac 47 | 48 | To run one of the devapps, run the command `go run src/test/devapps/*.go `. The devapp numbers are as follows: 49 | 50 | * 1 - `device_code_flow_sample.go` 51 | * 2 - `authorization_code_sample.go` 52 | * 3 - `username_password_sample.go` 53 | * 4 - `confidential_auth_code_sample.go` 54 | * 5 - `client_secret_sample.go` 55 | * 6 - `client_certificate_sample.go` 56 | -------------------------------------------------------------------------------- /apps/tests/devapps/authorization_code_sample.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | package main 5 | 6 | // TODO(msal expert): This should be refactored into an example maybe? 7 | // a "main" with a bunch of private functions that can't run isn't a good code sample. 8 | 9 | /* 10 | func getToken(w http.ResponseWriter, r *http.Request) { 11 | // Getting the authorization code from the URL's query 12 | states, ok := r.URL.Query()["state"] 13 | if !ok || len(states[0]) < 1 { 14 | log.Fatal(errors.New("State parameter missing, can't verify authorization code")) 15 | } 16 | codes, ok := r.URL.Query()["code"] 17 | if !ok || len(codes[0]) < 1 { 18 | log.Fatal(errors.New("Authorization code missing")) 19 | } 20 | if states[0] != config.State { 21 | log.Fatal(errors.New("State parameter is incorrect")) 22 | } 23 | code := codes[0] 24 | // Getting the access token using the authorization code 25 | result, err := publicClientApp.AcquireTokenByAuthCode(context.Background(), config.Scopes, &msal.AcquireTokenByAuthCodeOptions{ 26 | Code: code, 27 | CodeChallenge: config.CodeChallenge, 28 | }) 29 | if err != nil { 30 | log.Fatal(err) 31 | } 32 | // Prints the access token on the webpage 33 | fmt.Fprintf(w, "Access token is "+result.GetAccessToken()) 34 | } 35 | 36 | func acquireByAuthorizationCodePublic() { 37 | options := msal.DefaultPublicClientApplicationOptions() 38 | options.Authority = config.Authority 39 | publicClientApp, err := msal.NewPublicClientApplication(config.ClientID, &options) 40 | if err != nil { 41 | panic(err) 42 | } 43 | http.HandleFunc("/", redirectToURL) 44 | // The redirect uri set in our app's registration is http://localhost:port/redirect 45 | http.HandleFunc("/redirect", getToken) 46 | log.Fatal(http.ListenAndServe(":"+port, nil)) 47 | } 48 | 49 | func redirectToURL(w http.ResponseWriter, r *http.Request) { 50 | // Getting the URL to redirect to acquire the authorization code 51 | authCodeURLParams := msal.CreateAuthorizationCodeURLParameters(config.ClientID, config.RedirectURI, config.Scopes) 52 | authCodeURLParams.CodeChallenge = config.CodeChallenge 53 | authCodeURLParams.State = config.State 54 | authURL, err := publicClientApp.AuthCodeURL(context.Background(), authCodeURLParams) 55 | if err != nil { 56 | log.Fatal(err) 57 | } 58 | // Redirecting to the URL we have received 59 | log.Info(authURL) 60 | http.Redirect(w, r, authURL, http.StatusSeeOther) 61 | } 62 | */ 63 | -------------------------------------------------------------------------------- /apps/tests/devapps/client_certificate_sample.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | package main 5 | 6 | import ( 7 | "context" 8 | "fmt" 9 | "log" 10 | "os" 11 | 12 | "github.com/AzureAD/microsoft-authentication-library-for-go/apps/confidential" 13 | ) 14 | 15 | var _config2 *Config = CreateConfig("confidential_config.json") 16 | 17 | // Keep the ConfidentialClient application object around, because it maintains a token cache 18 | // For simplicity, the sample uses global variables. 19 | // For user flows (web site, web api) or for large multi-tenant apps use a cache per user or per tenant 20 | var _app2 *confidential.Client = createAppWithCert() 21 | 22 | func createAppWithCert() *confidential.Client { 23 | 24 | pemData, err := os.ReadFile(_config2.PemData) 25 | if err != nil { 26 | log.Fatal(err) 27 | } 28 | 29 | // This extracts our public certificates and private key from the PEM file. If it is 30 | // encrypted, the second argument must be password to decode. 31 | // IMPORTANT SECURITY NOTICE: never store passwords in code. The recommended pattern is to keep the certificate in a vault (e.g. Azure KeyVault) 32 | // and to download it when the application starts. 33 | certs, privateKey, err := confidential.CertFromPEM(pemData, "") 34 | if err != nil { 35 | log.Fatal(err) 36 | } 37 | cred, err := confidential.NewCredFromCert(certs, privateKey) 38 | if err != nil { 39 | log.Fatal(err) 40 | } 41 | app, err := confidential.New(_config2.Authority, _config2.ClientID, cred, confidential.WithCache(cacheAccessor)) 42 | if err != nil { 43 | log.Fatal(err) 44 | } 45 | return &app 46 | } 47 | 48 | func acquireTokenClientCertificate() { 49 | 50 | result, err := _app2.AcquireTokenByCredential(context.Background(), _config1.Scopes) 51 | if err != nil { 52 | log.Fatal(err) 53 | } 54 | 55 | fmt.Println("A Bearer token was acquired, it expires on: ", result.ExpiresOn) 56 | } 57 | -------------------------------------------------------------------------------- /apps/tests/devapps/client_secret_sample.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | package main 5 | 6 | import ( 7 | "context" 8 | "fmt" 9 | "log" 10 | 11 | "github.com/AzureAD/microsoft-authentication-library-for-go/apps/confidential" 12 | ) 13 | 14 | var _config1 *Config = CreateConfig("confidential_config.json") 15 | 16 | // Keep the ConfidentialClient application object around, because it maintains a token cache 17 | // For simplicity, the sample uses global variables. 18 | // For user flows (web site, web api) or for large multi-tenant apps use a cache per user or per tenant 19 | var _app1 *confidential.Client = createAppWithSecret() 20 | 21 | func createAppWithSecret() *confidential.Client { 22 | 23 | cred, err := confidential.NewCredFromSecret(_config1.ClientSecret) 24 | if err != nil { 25 | log.Fatal(err) 26 | } 27 | app, err := confidential.New(_config1.Authority, _config1.ClientID, cred) 28 | if err != nil { 29 | log.Fatal(err) 30 | } 31 | 32 | return &app 33 | } 34 | 35 | func acquireTokenClientSecret() { 36 | 37 | result, err := _app1.AcquireTokenByCredential(context.Background(), _config1.Scopes) 38 | if err != nil { 39 | log.Fatal(err) 40 | } 41 | 42 | fmt.Println("A Bearer token was acquired, it expires on: ", result.ExpiresOn) 43 | } 44 | -------------------------------------------------------------------------------- /apps/tests/devapps/confidential_auth_code_sample.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | package main 5 | 6 | /* 7 | var ( 8 | accessToken string 9 | confidentialConfig = CreateConfig("confidential_config.json") 10 | app confidential.Client 11 | ) 12 | 13 | // TODO(msal): I'm not sure what to do here with the CodeChallenge and State. authCodeURLParams 14 | // is no more. CodeChallenge is only used now in a confidential.AcquireTokenByAuthCode(), which 15 | // this is not using. Maybe now this is a two step process???? 16 | func redirectToURLConfidential(w http.ResponseWriter, r *http.Request) { 17 | // Getting the URL to redirect to acquire the authorization code 18 | authCodeURLParams.CodeChallenge = confidentialConfig.CodeChallenge 19 | authCodeURLParams.State = confidentialConfig.State 20 | authURL, err := app.AuthCodeURL(context.Background(), confidentialConfig.ClientID, confidentialConfig.RedirectURI, confidentialConfig.Scopes) 21 | if err != nil { 22 | http.Error(w, err.Error(), http.StatusUnauthorized) 23 | return 24 | } 25 | // Redirecting to the URL we have received 26 | log.Println("redirecting to auth: ", authURL) 27 | http.Redirect(w, r, authURL, http.StatusSeeOther) 28 | } 29 | 30 | func getTokenConfidential(w http.ResponseWriter, r *http.Request) { 31 | // Getting the authorization code from the URL's query 32 | states, ok := r.URL.Query()["state"] 33 | if !ok || len(states[0]) < 1 { 34 | log.Fatal(errors.New("State parameter missing, can't verify authorization code")) 35 | } 36 | codes, ok := r.URL.Query()["code"] 37 | if !ok || len(codes[0]) < 1 { 38 | log.Fatal(errors.New("Authorization code missing")) 39 | } 40 | if states[0] != config.State { 41 | log.Fatal(errors.New("State parameter is incorrect")) 42 | } 43 | code := codes[0] 44 | // Getting the access token using the authorization code 45 | result, err := app.AcquireTokenByAuthCode( 46 | context.Background(), 47 | confidentialConfig.Scopes, 48 | confidential.CodeChallenge(code, confidentialConfig.CodeChallenge), 49 | ) 50 | if err != nil { 51 | log.Fatal(err) 52 | } 53 | // Prints the access token on the webpage 54 | fmt.Fprintf(w, "Access token is "+result.GetAccessToken()) 55 | accessToken = result.GetAccessToken() 56 | } 57 | 58 | // TODO(msal): Needs to use an x509 certificate like the other now that we are not using a 59 | // thumbprint directly. 60 | /* 61 | func acquireByAuthorizationCodeConfidential(ctx context.Context) { 62 | key, err := os.ReadFile(confidentialConfig.KeyFile) 63 | if err != nil { 64 | log.Fatal(err) 65 | } 66 | 67 | certificate, err := msal.CreateClientCredentialFromCertificate(confidentialConfig.Thumbprint, key) 68 | if err != nil { 69 | log.Fatal(err) 70 | } 71 | 72 | options := msal.DefaultConfidentialClientApplicationOptions() 73 | options.Accessor = cacheAccessor 74 | options.Authority = confidentialConfig.Authority 75 | app, err := msal.NewConfidentialClientApplication(confidentialConfig.ClientID, certificate, &options) 76 | if err != nil { 77 | log.Fatal(err) 78 | } 79 | var userAccount shared.Account 80 | for _, account := range app.Accounts(ctx) { 81 | if account.PreferredUsername == confidentialConfig.Username { 82 | userAccount = account 83 | } 84 | } 85 | result, err := app.AcquireTokenSilent( 86 | context.Background(), 87 | confidentialConfig.Scopes, 88 | &msal.AcquireTokenSilentOptions{ 89 | Account: userAccount, 90 | }, 91 | ) 92 | if err != nil { 93 | panic(err) 94 | } 95 | fmt.Printf("Access token is " + result.GetAccessToken()) 96 | accessToken = result.GetAccessToken() 97 | 98 | http.HandleFunc("/", redirectToURLConfidential) 99 | // The redirect uri set in our app's registration is http://localhost:port/redirect 100 | http.HandleFunc("/redirect", getTokenConfidential) 101 | log.Fatal(http.ListenAndServe(":"+port, nil)) 102 | } 103 | */ 104 | -------------------------------------------------------------------------------- /apps/tests/devapps/confidential_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "authority": "https://login.microsoftonline.com/your_tenant_id", 3 | "client_id": "your_client_id", 4 | "scopes": ["requested_scope- usually of the format - {ResourceId}+/.default"], 5 | "redirect_uri": "redirect uri registered on the portal", 6 | "code_challenge": "transformed code verifier from PKCE", 7 | "state": "state parameter for authorization code flow", 8 | "client_secret": "client secret you generated for your app", 9 | "pem_file": "the file path of pem containing public cert and private key" 10 | } 11 | -------------------------------------------------------------------------------- /apps/tests/devapps/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "authority": "https://login.microsoftonline.com/organizations", 3 | "client_id": "your_client_id", 4 | "scopes": ["user.read"], 5 | "username": "your_username", 6 | "password": "your_password", 7 | "redirect_uri": "redirect uri registered on the portal", 8 | "code_challenge": "transformed code verifier from PKCE", 9 | "state": "state parameter for authorization code flow", 10 | "client_secret": "client secret you generated for your app", 11 | "thumbprint": "the certificate thumbprint defined in your app generation", 12 | "pem_file": "the file path of your private key pem" 13 | } -------------------------------------------------------------------------------- /apps/tests/devapps/device_code_flow_sample.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | package main 5 | 6 | import ( 7 | "context" 8 | "fmt" 9 | "time" 10 | 11 | "github.com/AzureAD/microsoft-authentication-library-for-go/apps/public" 12 | ) 13 | 14 | func acquireTokenDeviceCode() { 15 | config := CreateConfig("config.json") 16 | 17 | app, err := public.New(config.ClientID, public.WithCache(cacheAccessor), public.WithAuthority(config.Authority)) 18 | if err != nil { 19 | panic(err) 20 | } 21 | 22 | // look in the cache to see if the account to use has been cached 23 | var userAccount public.Account 24 | accounts, err := app.Accounts(context.Background()) 25 | if err != nil { 26 | panic("failed to read the cache") 27 | } 28 | for _, account := range accounts { 29 | if account.PreferredUsername == config.Username { 30 | userAccount = account 31 | } 32 | } 33 | // found a cached account, now see if an applicable token has been cached 34 | // NOTE: this API conflates error states, i.e. err is non-nil if an applicable token isn't 35 | // cached or if something goes wrong (making the HTTP request, unmarshalling, etc). 36 | authResult, err := app.AcquireTokenSilent(context.Background(), config.Scopes, public.WithSilentAccount(userAccount)) 37 | if err != nil { 38 | // either there was no cached account/token or the call to AcquireTokenSilent() failed 39 | // make a new request to AAD 40 | ctx, cancel := context.WithTimeout(context.Background(), 100*time.Second) 41 | defer cancel() 42 | devCode, err := app.AcquireTokenByDeviceCode(ctx, config.Scopes) 43 | if err != nil { 44 | panic(err) 45 | } 46 | fmt.Printf("Device Code is: %s\n", devCode.Result.Message) 47 | result, err := devCode.AuthenticationResult(ctx) 48 | if err != nil { 49 | panic(fmt.Sprintf("got error while waiting for user to input the device code: %s", err)) 50 | } 51 | fmt.Println("Access token is " + result.AccessToken) 52 | return 53 | } 54 | fmt.Println("Access token is " + authResult.AccessToken) 55 | } 56 | -------------------------------------------------------------------------------- /apps/tests/devapps/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | ) 6 | 7 | var ( 8 | //config = CreateConfig("config.json") reenable when config is implemented 9 | cacheAccessor = &TokenCache{file: "serialized_cache.json"} 10 | ) 11 | 12 | func main() { 13 | ctx := context.Background() 14 | 15 | // Choose a sammple to run. 16 | exampleType := "5" 17 | 18 | if exampleType == "1" { 19 | acquireTokenDeviceCode() 20 | /*} else if exampleType == "2" { 21 | acquireByAuthorizationCodePublic() 22 | */ 23 | } else if exampleType == "3" { 24 | // This sample uses a serialized cache in an ecrypted file on Windows / KeyChain on Mac / KeyRing on Linux 25 | acquireByUsernamePasswordPublic(ctx) 26 | } else if exampleType == "4" { 27 | panic("currently not implemented") 28 | //acquireByAuthorizationCodeConfidential() 29 | } else if exampleType == "5" { 30 | // This sample does not use a serialized cache - it relies on in-memory cache by reusing the app object 31 | // This works well for app tokens, because there is only 1 token per resource, per tenant. 32 | acquireTokenClientSecret() 33 | 34 | // this time the token comes from the cache! 35 | acquireTokenClientSecret() 36 | } else if exampleType == "6" { 37 | // This sample does not use a serialized cache - it relies on in-memory cache by reusing the app object 38 | // This works well for app tokens, because there is only 1 token per resource, per tenant. 39 | acquireTokenClientCertificate() 40 | 41 | // this time the token comes from the cache! 42 | acquireTokenClientCertificate() 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /apps/tests/devapps/managedidentity/docs/msi_manual_testing.md: -------------------------------------------------------------------------------- 1 | # Running Managed Identity Sources 2 | 3 | A full overview of how to run each sample source can be found in the [Azure Samples - MSAL GO](https://github.com/Azure-Samples/msal-managed-identity/tree/main/src/go) repository 4 | -------------------------------------------------------------------------------- /apps/tests/devapps/sample_cache_accessor.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | package main 5 | 6 | import ( 7 | "context" 8 | "log" 9 | "os" 10 | 11 | "github.com/AzureAD/microsoft-authentication-library-for-go/apps/cache" 12 | ) 13 | 14 | type TokenCache struct { 15 | file string 16 | } 17 | 18 | func (t *TokenCache) Replace(ctx context.Context, cache cache.Unmarshaler, hints cache.ReplaceHints) error { 19 | data, err := os.ReadFile(t.file) 20 | if err != nil { 21 | log.Println(err) 22 | } 23 | return cache.Unmarshal(data) 24 | } 25 | 26 | func (t *TokenCache) Export(ctx context.Context, cache cache.Marshaler, hints cache.ExportHints) error { 27 | data, err := cache.Marshal() 28 | if err != nil { 29 | log.Println(err) 30 | } 31 | return os.WriteFile(t.file, data, 0600) 32 | } 33 | -------------------------------------------------------------------------------- /apps/tests/devapps/sample_utils.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | package main 5 | 6 | import ( 7 | "encoding/json" 8 | "log" 9 | "os" 10 | ) 11 | 12 | // Config represents the config.json required to run the samples 13 | type Config struct { 14 | ClientID string `json:"client_id"` 15 | Authority string `json:"authority"` 16 | Scopes []string `json:"scopes"` 17 | Username string `json:"username"` 18 | Password string `json:"password"` 19 | RedirectURI string `json:"redirect_uri"` 20 | CodeChallenge string `json:"code_challenge"` 21 | CodeChallengeMethod string `json:"code_challenge_method"` 22 | State string `json:"state"` 23 | ClientSecret string `json:"client_secret"` 24 | Thumbprint string `json:"thumbprint"` 25 | PemData string `json:"pem_file"` 26 | } 27 | 28 | // CreateConfig creates the Config struct from a json file. 29 | func CreateConfig(fileName string) *Config { 30 | data, err := os.ReadFile(fileName) 31 | if err != nil { 32 | log.Fatal(err) 33 | } 34 | 35 | config := &Config{} 36 | err = json.Unmarshal(data, config) 37 | if err != nil { 38 | log.Fatal(err) 39 | } 40 | return config 41 | } 42 | -------------------------------------------------------------------------------- /apps/tests/devapps/serialized_cache.json: -------------------------------------------------------------------------------- 1 | { 2 | "Account": { 3 | "uid.utid-login.windows.net-contoso": { 4 | "username": "John Doe", 5 | "local_account_id": "object1234", 6 | "realm": "contoso", 7 | "environment": "login.windows.net", 8 | "home_account_id": "uid.utid", 9 | "authority_type": "MSSTS" 10 | } 11 | }, 12 | "RefreshToken": { 13 | "uid.utid-login.windows.net-refreshtoken-my_client_id--s2 s1 s3": { 14 | "target": "s2 s1 s3", 15 | "environment": "login.windows.net", 16 | "credential_type": "RefreshToken", 17 | "secret": "a refresh token", 18 | "client_id": "my_client_id", 19 | "home_account_id": "uid.utid" 20 | } 21 | }, 22 | "AccessToken": { 23 | "uid.utid-login.windows.net-accesstoken-my_client_id-contoso-s2 s1 s3": { 24 | "environment": "login.windows.net", 25 | "credential_type": "AccessToken", 26 | "secret": "an access token", 27 | "realm": "contoso", 28 | "target": "s2 s1 s3", 29 | "client_id": "my_client_id", 30 | "cached_at": "1000", 31 | "home_account_id": "uid.utid", 32 | "extended_expires_on": "4600", 33 | "expires_on": "4600", 34 | "token_type": "Bearer" 35 | } 36 | }, 37 | "IdToken": { 38 | "uid.utid-login.windows.net-idtoken-my_client_id-contoso-": { 39 | "realm": "contoso", 40 | "environment": "login.windows.net", 41 | "credential_type": "IdToken", 42 | "secret": "header.eyJvaWQiOiAib2JqZWN0MTIzNCIsICJwcmVmZXJyZWRfdXNlcm5hbWUiOiAiSm9obiBEb2UiLCAic3ViIjogInN1YiJ9.signature", 43 | "client_id": "my_client_id", 44 | "home_account_id": "uid.utid" 45 | } 46 | }, 47 | "AppMetadata": { 48 | "AppMetadata-login.windows.net-my_client_id": { 49 | "environment": "login.windows.net", 50 | "family_id": null, 51 | "client_id": "my_client_id" 52 | } 53 | } 54 | } -------------------------------------------------------------------------------- /apps/tests/devapps/username_password_sample.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | package main 5 | 6 | import ( 7 | "context" 8 | "fmt" 9 | "log" 10 | 11 | "github.com/AzureAD/microsoft-authentication-library-for-go/apps/public" 12 | ) 13 | 14 | func acquireByUsernamePasswordPublic(ctx context.Context) { 15 | config := CreateConfig("config.json") 16 | app, err := public.New(config.ClientID, public.WithCache(cacheAccessor), public.WithAuthority(config.Authority)) 17 | if err != nil { 18 | panic(err) 19 | } 20 | 21 | // look in the cache to see if the account to use has been cached 22 | var userAccount public.Account 23 | accounts, err := app.Accounts(ctx) 24 | if err != nil { 25 | panic("failed to read the cache") 26 | } 27 | for _, account := range accounts { 28 | if account.PreferredUsername == config.Username { 29 | userAccount = account 30 | } 31 | } 32 | // found a cached account, now see if an applicable token has been cached 33 | // NOTE: this API conflates error states, i.e. err is non-nil if an applicable token isn't 34 | // cached or if something goes wrong (making the HTTP request, unmarshalling, etc). 35 | result, err := app.AcquireTokenSilent( 36 | context.Background(), 37 | config.Scopes, 38 | public.WithSilentAccount(userAccount), 39 | ) 40 | if err != nil { 41 | // either there's no applicable token in the cache or something failed 42 | log.Println(err) 43 | // either there was no cached account/token or the call to AcquireTokenSilent() failed 44 | // make a new request to AAD 45 | result, err = app.AcquireTokenByUsernamePassword( 46 | context.Background(), 47 | config.Scopes, 48 | config.Username, 49 | config.Password, 50 | ) 51 | if err != nil { 52 | log.Fatal(err) 53 | } 54 | } 55 | fmt.Println("Access token is " + result.AccessToken) 56 | } 57 | -------------------------------------------------------------------------------- /apps/tests/integration/README.md: -------------------------------------------------------------------------------- 1 | # Go Integration Test 2 | 3 | This guide explains how to: 4 | 5 | 1. Download a certificate from [link](https://ms.portal.azure.com/#@microsoft.onmicrosoft.com/asset/Microsoft_Azure_KeyVault/Certificate/https://msidlabs.vault.azure.net/certificates/LabAuth). 6 | 2. Download the `.pex/.pem` format 7 | 3. Convert the `.cert` file to `.pem` file. 8 | 4. Execute Go integration tests. 9 | 10 | ## Prerequisites 11 | 12 | - Run `openssl pkcs12 -in /cert.pfx -out /cert.pem -nodes -passin pass:''` 13 | - It should be in the root folder of the `microsoft-authentication-library-for-go` 14 | 15 | ## Steps 16 | 17 | ### 1. Running the tests 18 | 19 | ```bash 20 | go test -race ./apps/tests/integration/ 21 | ``` 22 | -------------------------------------------------------------------------------- /apps/tests/integration/cache_accessor.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | package integration 5 | 6 | import ( 7 | "context" 8 | "log" 9 | "os" 10 | 11 | "github.com/AzureAD/microsoft-authentication-library-for-go/apps/cache" 12 | ) 13 | 14 | type TokenCache struct { 15 | file string 16 | } 17 | 18 | func (t *TokenCache) Replace(ctx context.Context, cache cache.Unmarshaler, hints cache.ReplaceHints) error { 19 | data, err := os.ReadFile(t.file) 20 | if err != nil { 21 | log.Println(err) 22 | } 23 | return cache.Unmarshal(data) 24 | } 25 | 26 | func (t *TokenCache) Export(ctx context.Context, cache cache.Marshaler, hints cache.ExportHints) error { 27 | data, err := cache.Marshal() 28 | if err != nil { 29 | log.Println(err) 30 | } 31 | return os.WriteFile(t.file, data, 0600) 32 | } 33 | 34 | func (t *TokenCache) Print() string { 35 | data, err := os.ReadFile(t.file) 36 | if err != nil { 37 | return err.Error() 38 | } 39 | return string(data) 40 | } 41 | -------------------------------------------------------------------------------- /apps/tests/performance/performance_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | package performance 5 | 6 | import ( 7 | "context" 8 | "fmt" 9 | "math/rand" 10 | "os" 11 | "testing" 12 | "time" 13 | 14 | "github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/base" 15 | "github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/oauth" 16 | "github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/oauth/fake" 17 | "github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/oauth/ops/accesstokens" 18 | "github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/oauth/ops/authority" 19 | "github.com/montanaflynn/stats" 20 | ) 21 | 22 | func fakeClient() (base.Client, error) { 23 | // we use a base.Client so we can provide a fake OAuth client 24 | return base.New("fake_client_id", "https://fake_authority/my_utid", &oauth.Client{ 25 | Authority: &fake.Authority{ 26 | InstanceResp: authority.InstanceDiscoveryResponse{ 27 | Metadata: []authority.InstanceDiscoveryMetadata{ 28 | { 29 | PreferredNetwork: "fake_authority", 30 | Aliases: []string{"fake_authority"}, 31 | }, 32 | }, 33 | }, 34 | }, 35 | Resolver: &fake.ResolveEndpoints{ 36 | Endpoints: authority.Endpoints{ 37 | AuthorizationEndpoint: "auth_endpoint", 38 | TokenEndpoint: "token_endpoint", 39 | }, 40 | }, 41 | WSTrust: &fake.WSTrust{}, 42 | }) 43 | } 44 | 45 | func populateCache(users int, tokens int, authParams authority.AuthParams, client base.Client) { 46 | for user := 0; user < users; user++ { 47 | for token := 0; token < tokens; token++ { 48 | authParams := client.AuthParams 49 | authParams.UserAssertion = fmt.Sprintf("fake_access_token%d", user) 50 | authParams.AuthorizationType = authority.ATOnBehalfOf 51 | scope := fmt.Sprintf("scope%d", token) 52 | 53 | _, err := client.AuthResultFromToken(context.Background(), authParams, accesstokens.TokenResponse{ 54 | AccessToken: fmt.Sprintf("fake_access_token%d", user), 55 | RefreshToken: "fake_refresh_token", 56 | ClientInfo: accesstokens.ClientInfo{UID: "my_uid", UTID: fmt.Sprintf("%dmy_utid", user)}, 57 | ExpiresOn: time.Now().Add(1 * time.Hour), 58 | GrantedScopes: accesstokens.Scopes{Slice: []string{scope}}, 59 | IDToken: accesstokens.IDToken{ 60 | RawToken: "x.e30", 61 | }, 62 | }) 63 | if err != nil { 64 | panic(err) 65 | } 66 | } 67 | } 68 | } 69 | func calculateStats(users, tokens int, duration []float64) { 70 | 71 | fmt.Printf("No of users: %d, No of tokens per user: %d \n", users, tokens) 72 | 73 | mean, err := stats.Mean(duration) 74 | if err != nil { 75 | panic(err) 76 | } 77 | meanTime := mean / float64(time.Microsecond) 78 | fmt.Println("Mean") 79 | fmt.Println(meanTime) 80 | 81 | median, err := stats.Median(duration) 82 | medianTime := median / float64(time.Microsecond) 83 | if err != nil { 84 | panic(err) 85 | } 86 | fmt.Println("Median") 87 | fmt.Println(medianTime) 88 | 89 | stdDev, err := stats.StandardDeviation(duration) 90 | stdDevTime := stdDev / float64(time.Microsecond) 91 | if err != nil { 92 | panic(err) 93 | } 94 | fmt.Println("Standard Deviation") 95 | fmt.Println(stdDevTime) 96 | 97 | min, err := stats.Min(duration) 98 | minTime := min / float64(time.Microsecond) 99 | if err != nil { 100 | panic(err) 101 | } 102 | fmt.Println("Min Time") 103 | fmt.Println(minTime) 104 | 105 | max, err := stats.Max(duration) 106 | maxTime := max / float64(time.Microsecond) 107 | if err != nil { 108 | panic(err) 109 | } 110 | fmt.Println("Max Time") 111 | fmt.Println(maxTime) 112 | 113 | } 114 | 115 | func benchMarkObo(users int, tokens int, client base.Client) { 116 | var duration []float64 117 | for start := time.Now(); time.Since(start) < time.Minute*1; { 118 | s := time.Now() 119 | queryCache(users, tokens, client) 120 | e := time.Now() 121 | duration = append(duration, float64(e.Sub(s))) 122 | } 123 | calculateStats(users, tokens, duration) 124 | } 125 | 126 | func queryCache(users int, tokens int, client base.Client) { 127 | userAssertion := fmt.Sprintf("fake_access_token%d", rand.Intn(users)) 128 | scope := []string{fmt.Sprintf("scope%d", rand.Intn(tokens))} 129 | params := base.AcquireTokenOnBehalfOfParameters{ 130 | Scopes: scope, 131 | UserAssertion: userAssertion, 132 | Credential: &accesstokens.Credential{Secret: "fake_secret"}, 133 | } 134 | _, err := client.AcquireTokenOnBehalfOf(context.Background(), params) 135 | if err != nil { 136 | panic(err) 137 | } 138 | } 139 | func TestOnBehalfOfCacheTests(t *testing.T) { 140 | if os.Getenv("CI") != "" { 141 | t.Skip("Skipping testing in CI environment") 142 | } 143 | tests := []struct { 144 | Users int 145 | Tokens int 146 | }{ 147 | {1, 10000}, 148 | {1, 100000}, 149 | {100, 10000}, 150 | {1000, 10000}, 151 | {10000, 100}, 152 | } 153 | 154 | for _, test := range tests { 155 | client, err := fakeClient() 156 | if err != nil { 157 | panic(err) 158 | } 159 | authParams := client.AuthParams 160 | populateCache(test.Users, test.Tokens, authParams, client) 161 | benchMarkObo(test.Users, test.Tokens, client) 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | This library uses GitHub releases to document the changelog. See https://github.com/AzureAD/microsoft-authentication-library-for-go/releases 2 | -------------------------------------------------------------------------------- /docs/managedidentity_public_api.md: -------------------------------------------------------------------------------- 1 | # Managed Identity Public API Design Specification 2 | 3 | The purpose of this file is to go over the changes required for adding the Managed Identity feature to MSAL GO 4 | 5 | ## Public API 6 | 7 | The public API will be quite small. Based on the Java and .NET implementations, there is only 1 exposed method, **acquireTokenForManagedIdentity()** 8 | 9 | ```go 10 | // Acquires tokens from the configured managed identity on an azure resource. 11 | // 12 | // Resource: scopes application is requesting access to 13 | // Options: [WithClaims] 14 | func (client Client) AcquireToken(context context.Context, resource string, options ...AcquireTokenOption) (base.AuthResult, error) { 15 | return base.AuthResult{}, nil 16 | } 17 | 18 | // Source represents the managed identity sources supported. 19 | type Source int 20 | 21 | const ( 22 | // AzureArc represents the source to acquire token for managed identity is Azure Arc. 23 | AzureArc = 0 24 | 25 | // DefaultToIMDS indicates that the source is defaulted to IMDS since no environment variables are set. 26 | DefaultToIMDS = 1 27 | ) 28 | 29 | // Detects and returns the managed identity source available on the environment. 30 | func GetSource() Source { 31 | return DefaultToIMDS 32 | } 33 | ``` 34 | 35 | The end user simply needs to create their own instance of Managed Identity Client, i.e **managedIdentity.Client()**, passing in the **ManagedIdentityType** they want to use, and then call the public API. The example below shows creation of different clients for each of the different Managed Identity Types 36 | 37 | ```go 38 | import ( 39 | "context" 40 | "fmt" 41 | "net/http" 42 | 43 | mi "github.com/AzureAD/microsoft-authentication-library-for-go/apps/managedidentity" 44 | ) 45 | 46 | func RunManagedIdentity() { 47 | customHttpClient := &http.Client{} 48 | 49 | miSystemAssigned, error := mi.New(mi.SystemAssigned()) 50 | if error != nil { 51 | fmt.Println(error) 52 | } 53 | 54 | miClientIdAssigned, error := mi.New(mi.ClientID("client id 123"), mi.WithHTTPClient(customHttpClient)) 55 | if error != nil { 56 | fmt.Println(error) 57 | } 58 | 59 | miResourceIdAssigned, error := mi.New(mi.ResourceID("resource id 123")) 60 | if error != nil { 61 | fmt.Println(error) 62 | } 63 | 64 | miObjectIdAssigned, error := mi.New(mi.ObjectID("object id 123")) 65 | if error != nil { 66 | fmt.Println(error) 67 | } 68 | 69 | miSystemAssigned.AcquireToken(context.Background(), "resource", mi.WithClaims("claim")) 70 | 71 | miClientIdAssigned.AcquireToken(context.Background(), "resource") 72 | 73 | miResourceIdAssigned.AcquireToken(context.Background(), "resource", mi.WithClaims("claim")) 74 | 75 | miObjectIdAssigned.AcquireToken(context.Background(), "resource") 76 | } 77 | ``` 78 | 79 | To create a new **ManagedIdentityClient** 80 | 81 | ```go 82 | // Client to be used to acquire tokens for managed identity. 83 | // ID: [SystemAssigned()], [ClientID("clientID")], [ResourceID("resourceID")], [ObjectID("objectID")] 84 | // 85 | // Options: [WithHTTPClient] 86 | func New(id ID, options ...Option) (Client, error) { 87 | // implementation details 88 | } 89 | ``` 90 | 91 | The options available for passing to the client are 92 | 93 | ```go 94 | // WithHTTPClient allows for a custom HTTP client to be set. 95 | func WithHTTPClient(httpClient ops.HTTPClient) Option { 96 | // implementation details 97 | } 98 | ``` 99 | 100 | The options available for the request are 101 | 102 | ```go 103 | // WithClaims sets additional claims to request for the token, such as those required by conditional access policies. 104 | // Use this option when Azure AD returned a claims challenge for a prior request. The argument must be decoded. 105 | func WithClaims(claims string) AcquireTokenOption { 106 | // implementation details 107 | } 108 | ``` 109 | 110 | ## Error Handling 111 | 112 | Error handling in GO is different to what we used to in languages like Java or Swift. 113 | There is no concept of ‘exceptions’, instead we just return errors and immediately check if an error was returned and handle it there and then. 114 | The SDK will return client-side errors like so: 115 | 116 | ```go 117 | if err != nil { 118 | return errors.New("Some Managed Identity Error here”) 119 | } 120 | ``` 121 | 122 | This will be inside of any client methods that throw errors, using descriptive errors based on the .NET and Java Implementation. These errors will be propagated down the chain and handled when they are received 123 | 124 | For service side errors it works a little differently 125 | 126 | ```go 127 | switch reply.StatusCode { 128 | case 200, 201: 129 | default: 130 | sd := strings.TrimSpace(string(data)) 131 | 132 | if sd != "" { 133 | // We probably have the error in the body. 134 | return nil, errors.CallErr { 135 | Req: req, 136 | Resp: reply, 137 | Err: fmt.Errorf("http call(%s)(%s) error: reply status code was %d:\n%s",req.URL.String(), req.Method, reply.StatusCode, sd) 138 | } 139 | } 140 | 141 | return nil, errors.CallErr{ 142 | Req: req, 143 | Resp: reply, 144 | Err: fmt.Errorf("http call(%s)(%s) error: reply status code was %d", req.URL.String(), req.Method, reply.StatusCode), 145 | } 146 | } 147 | ``` 148 | 149 | In this example, you can see we are returning **errors.CallErr(Req: httpRequest, Resp: httpResponse, Err: error)** 150 | 151 | For the service side errors we have a struct object like this: 152 | 153 | ```go 154 | type CallErr struct { 155 | Req *http.Request 156 | // Resp contains response body 157 | Resp *http.Response 158 | Err error 159 | } 160 | ``` 161 | 162 | This structure should be followed for future service calls. More information on this implementation can be found [here](https://github.com/AzureAD/microsoft-authentication-library-for-go/blob/ae2db6b72c7010958355f448e99209bd28e76e67/apps/errors/error_design.md#L1) 163 | 164 | ## Caching 165 | 166 | Other MSALs have an Enum called **TokenSource** that lets us differentiate between **IdentityProvider**, **Cache** and **Broker**. 167 | 168 | Since GO does not have Brokers, we have created a PR [here](https://github.com/AzureAD/microsoft-authentication-library-for-go/pull/498) that adds a **AuthenticationResultMetadata** class to the **_base.go_** instance of **AuthResult** 169 | 170 | This **AuthenticationResultMetadata** contains the **TokenSource** and **RefreshOn** values, like .NET and Java implementations. The **TokenSource** here does not contain the broker field as it is not something that is planned currently 171 | 172 | ```go 173 | type TokenSource int 174 | 175 | const ( 176 | IdentityProvider TokenSource = 0 177 | Cache = 1 178 | ) 179 | 180 | type AuthResultMetadata struct { 181 | TokenSource TokenSource 182 | RefreshOn time.Time 183 | } 184 | ``` 185 | 186 | ## FIC Support 187 | 188 | You can review information on FIC [here](https://review.learn.microsoft.com/en-us/identity/microsoft-identity-platform/federated-identity-credentials?branch=main&tabs=dotnet) 189 | 190 | Managed Identity abstracts the complexity of certificates away, by virtue of being hosted on an Azure VM you get access to the services you need i.e. key vault 191 | 192 | Managed Identity is a single tenant. This is an issue as Microsoft has many multi tenanted apps. 193 | FIC solves this by allowing you to declare a trust relationship with an identity provider and application i.e. ‘I trust this GitHub token, if I see this Git Hub token, give me a token for something I want access to i.e. Key Vault’ 194 | So, if you can get a token for Managed Identity you can use it to access the key vault in all tenants 195 | 196 | Right now, we shouldn’t have to do anything. 197 | Currently FIC would be the token for the certificate in **acquireTokenByCredential()**, we would just provide the token for ManagedIdentity instead of using the certificate 198 | 199 | This is a 2-step process: 200 | 201 | 1. Get token for Managed Identity. Would be a special token for a specific scope. 202 | 203 | 2. Create a confidential client and get a token. Will get an API certificate for the assertion, and use the Managed Identity token instead of the certificate 204 | 205 | All we need to do for now is test FIC with Managed Identity, and update any documentation to go along with it -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/AzureAD/microsoft-authentication-library-for-go 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/golang-jwt/jwt/v5 v5.2.2 7 | github.com/google/uuid v1.3.0 8 | github.com/kylelemons/godebug v1.1.0 9 | github.com/montanaflynn/stats v0.7.0 10 | github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 11 | ) 12 | 13 | require golang.org/x/sys v0.5.0 // indirect 14 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= 2 | github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= 3 | github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= 4 | github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 5 | github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= 6 | github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= 7 | github.com/montanaflynn/stats v0.7.0 h1:r3y12KyNxj/Sb/iOE46ws+3mS1+MZca1wlHQFPsY/JU= 8 | github.com/montanaflynn/stats v0.7.0/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= 9 | github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU= 10 | github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= 11 | golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 12 | golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= 13 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 14 | --------------------------------------------------------------------------------