├── .Rbuildignore ├── .github └── workflows │ └── check-standard.yaml ├── .gitignore ├── CONTRIBUTING.md ├── DESCRIPTION ├── LICENSE ├── LICENSE.md ├── NAMESPACE ├── NEWS.md ├── R ├── AzureAuth.R ├── AzureToken.R ├── cert_creds.R ├── classes.R ├── flow_init.R ├── format.R ├── initfuncs.R ├── jwt.R ├── managed_token.R ├── normalize.R ├── token.R └── utils.R ├── README.md ├── SECURITY.md ├── man ├── AzureR_dir.Rd ├── AzureToken.Rd ├── authorization.Rd ├── cert_assertion.Rd ├── figures │ └── logo.png ├── format.Rd ├── get_azure_token.Rd ├── guid.Rd └── jwt.Rd ├── tests ├── testthat.R └── testthat │ ├── test00_normalize.R │ ├── test02_jwt.R │ ├── test10_v1_token.R │ ├── test11_v1_token_misc.R │ ├── test20_v2_token.R │ └── test21_v2_token_misc.R └── vignettes ├── images ├── authcode.png ├── clientcred.png └── devicecode.png ├── scenarios.Rmd ├── shiny.Rmd └── token.Rmd /.Rbuildignore: -------------------------------------------------------------------------------- 1 | ^misc$ 2 | ^\.vs$ 3 | \.sln$ 4 | \.Rproj$ 5 | \.Rxproj$ 6 | ^\.Rproj\.user$ 7 | .travis.yml 8 | CONTRIBUTING.md 9 | drat.sh 10 | ^LICENSE\.md$ 11 | ^\.github$ 12 | -------------------------------------------------------------------------------- /.github/workflows/check-standard.yaml: -------------------------------------------------------------------------------- 1 | # For help debugging build failures open an issue on the RStudio community with the 'github-actions' tag. 2 | # https://community.rstudio.com/new-topic?category=Package%20development&tags=github-actions 3 | on: [push, pull_request] 4 | 5 | name: R-CMD-check 6 | 7 | jobs: 8 | R-CMD-check: 9 | if: github.repository_owner != 'cloudyr' 10 | runs-on: ${{ matrix.config.os }} 11 | 12 | name: ${{ matrix.config.os }} (${{ matrix.config.r }}) 13 | 14 | strategy: 15 | fail-fast: false 16 | matrix: 17 | config: 18 | - {os: windows-latest, r: 'release'} 19 | - {os: macOS-latest, r: 'release'} 20 | - {os: ubuntu-20.04, r: 'release', rspm: "https://packagemanager.rstudio.com/cran/__linux__/focal/latest"} 21 | 22 | env: 23 | R_REMOTES_NO_ERRORS_FROM_WARNINGS: true 24 | RSPM: ${{ matrix.config.rspm }} 25 | 26 | steps: 27 | - uses: actions/checkout@v2 28 | with: 29 | fetch-depth: 0 # required for mirroring, see https://stackoverflow.com/a/64272409/474349 30 | 31 | - name: Copy to Cloudyr 32 | if: runner.os == 'Linux' && github.ref == 'refs/heads/master' && github.repository_owner == 'Azure' 33 | env: 34 | token: "${{ secrets.ghPat }}" 35 | # git config hack required, see https://stackoverflow.com/q/64270867/474349 36 | run: | 37 | export CLOUDYR_REPO=$(echo $GITHUB_REPOSITORY | sed "s/Azure/cloudyr/") 38 | git config -l | grep 'http\..*\.extraheader' | cut -d= -f1 | \ 39 | xargs -L1 git config --unset-all 40 | git push --prune https://token:$token@github.com/${CLOUDYR_REPO}.git +refs/remotes/origin/*:refs/heads/* +refs/tags/*:refs/tags/* 41 | 42 | - uses: r-lib/actions/setup-r@master 43 | with: 44 | r-version: ${{ matrix.config.r }} 45 | 46 | - uses: r-lib/actions/setup-pandoc@master 47 | 48 | - name: Query dependencies 49 | run: | 50 | install.packages('remotes') 51 | saveRDS(remotes::dev_package_deps(dependencies = TRUE), ".github/depends.Rds", version = 2) 52 | writeLines(sprintf("R-%i.%i", getRversion()$major, getRversion()$minor), ".github/R-version") 53 | shell: Rscript {0} 54 | 55 | - name: Cache R packages 56 | if: runner.os != 'Windows' 57 | uses: actions/cache@v2 58 | with: 59 | path: ${{ env.R_LIBS_USER }} 60 | key: ${{ runner.os }}-${{ hashFiles('.github/R-version') }}-1-${{ hashFiles('.github/depends.Rds') }} 61 | restore-keys: ${{ runner.os }}-${{ hashFiles('.github/R-version') }}-1- 62 | 63 | - name: Install system dependencies 64 | if: runner.os == 'Linux' 65 | run: | 66 | while read -r cmd 67 | do 68 | eval sudo $cmd 69 | done < <(Rscript -e 'writeLines(remotes::system_requirements("ubuntu", "20.04"))') 70 | 71 | - name: Install dependencies 72 | run: | 73 | remotes::install_deps(dependencies = TRUE) 74 | remotes::install_cran(c("pkgbuild", "rcmdcheck", "drat")) 75 | shell: Rscript {0} 76 | 77 | - name: Check 78 | env: 79 | _R_CHECK_CRAN_INCOMING_REMOTE_: false 80 | _R_CHECK_FORCE_SUGGESTS_: false 81 | run: | 82 | pkg <- pkgbuild::build() 83 | rcmdcheck::rcmdcheck(pkg, args = c("--no-manual", "--as-cran"), error_on = "warning", check_dir = "check") 84 | shell: Rscript {0} 85 | 86 | - name: Upload check results 87 | if: failure() 88 | uses: actions/upload-artifact@main 89 | with: 90 | name: ${{ runner.os }}-r${{ matrix.config.r }}-results 91 | path: check 92 | 93 | - name: Update Cloudyr drat 94 | if: success() && runner.os == 'Linux' && github.ref == 'refs/heads/master' && github.repository_owner == 'Azure' 95 | env: 96 | token: "${{ secrets.ghPat }}" 97 | run: | 98 | cd .. 99 | export PKGBUILD_GZ=$(ls *.gz) 100 | mkdir drat 101 | cd drat 102 | git init 103 | git config user.email "dummy@example.com" 104 | git config user.name "Github Actions" 105 | git remote add upstream "https://token:$token@github.com/cloudyr/cloudyr.github.io.git" 106 | git fetch upstream 107 | git checkout master 108 | Rscript -e "drat::insertPackage('../$PKGBUILD_GZ', repodir='./drat')" 109 | git add --all 110 | git commit -m "add $PKGBUILD_GZ (build $GITHUB_RUN_NUMBER)" 111 | git push 112 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | x64/ 19 | x86/ 20 | bld/ 21 | [Bb]in/ 22 | [Oo]bj/ 23 | [Ll]og/ 24 | 25 | # Visual Studio 2015 cache/options directory 26 | .vs/ 27 | # Uncomment if you have tasks that create the project's static files in wwwroot 28 | #wwwroot/ 29 | 30 | # MSTest test Results 31 | [Tt]est[Rr]esult*/ 32 | [Bb]uild[Ll]og.* 33 | 34 | # NUNIT 35 | *.VisualState.xml 36 | TestResult.xml 37 | 38 | # Build Results of an ATL Project 39 | [Dd]ebugPS/ 40 | [Rr]eleasePS/ 41 | dlldata.c 42 | 43 | # DNX 44 | project.lock.json 45 | project.fragment.lock.json 46 | artifacts/ 47 | 48 | *_i.c 49 | *_p.c 50 | *_i.h 51 | *.ilk 52 | *.meta 53 | *.obj 54 | *.pch 55 | *.pdb 56 | *.pgc 57 | *.pgd 58 | *.rsp 59 | *.sbr 60 | *.tlb 61 | *.tli 62 | *.tlh 63 | *.tmp 64 | *.tmp_proj 65 | *.log 66 | *.vspscc 67 | *.vssscc 68 | .builds 69 | *.pidb 70 | *.svclog 71 | *.scc 72 | 73 | # Chutzpah Test files 74 | _Chutzpah* 75 | 76 | # Visual C++ cache files 77 | ipch/ 78 | *.aps 79 | *.ncb 80 | *.opendb 81 | *.opensdf 82 | *.sdf 83 | *.cachefile 84 | *.VC.db 85 | *.VC.VC.opendb 86 | 87 | # Visual Studio profiler 88 | *.psess 89 | *.vsp 90 | *.vspx 91 | *.sap 92 | 93 | # TFS 2012 Local Workspace 94 | $tf/ 95 | 96 | # Guidance Automation Toolkit 97 | *.gpState 98 | 99 | # ReSharper is a .NET coding add-in 100 | _ReSharper*/ 101 | *.[Rr]e[Ss]harper 102 | *.DotSettings.user 103 | 104 | # JustCode is a .NET coding add-in 105 | .JustCode 106 | 107 | # TeamCity is a build add-in 108 | _TeamCity* 109 | 110 | # DotCover is a Code Coverage Tool 111 | *.dotCover 112 | 113 | # NCrunch 114 | _NCrunch_* 115 | .*crunch*.local.xml 116 | nCrunchTemp_* 117 | 118 | # MightyMoose 119 | *.mm.* 120 | AutoTest.Net/ 121 | 122 | # Web workbench (sass) 123 | .sass-cache/ 124 | 125 | # Installshield output folder 126 | [Ee]xpress/ 127 | 128 | # DocProject is a documentation generator add-in 129 | DocProject/buildhelp/ 130 | DocProject/Help/*.HxT 131 | DocProject/Help/*.HxC 132 | DocProject/Help/*.hhc 133 | DocProject/Help/*.hhk 134 | DocProject/Help/*.hhp 135 | DocProject/Help/Html2 136 | DocProject/Help/html 137 | 138 | # Click-Once directory 139 | publish/ 140 | 141 | # Publish Web Output 142 | *.[Pp]ublish.xml 143 | *.azurePubxml 144 | # TODO: Comment the next line if you want to checkin your web deploy settings 145 | # but database connection strings (with potential passwords) will be unencrypted 146 | #*.pubxml 147 | *.publishproj 148 | 149 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 150 | # checkin your Azure Web App publish settings, but sensitive information contained 151 | # in these scripts will be unencrypted 152 | PublishScripts/ 153 | 154 | # NuGet Packages 155 | *.nupkg 156 | # The packages folder can be ignored because of Package Restore 157 | **/packages/* 158 | # except build/, which is used as an MSBuild target. 159 | !**/packages/build/ 160 | # Uncomment if necessary however generally it will be regenerated when needed 161 | #!**/packages/repositories.config 162 | # NuGet v3's project.json files produces more ignoreable files 163 | *.nuget.props 164 | *.nuget.targets 165 | 166 | # Microsoft Azure Build Output 167 | csx/ 168 | *.build.csdef 169 | 170 | # Microsoft Azure Emulator 171 | ecf/ 172 | rcf/ 173 | 174 | # Windows Store app package directories and files 175 | AppPackages/ 176 | BundleArtifacts/ 177 | Package.StoreAssociation.xml 178 | _pkginfo.txt 179 | 180 | # Visual Studio cache files 181 | # files ending in .cache can be ignored 182 | *.[Cc]ache 183 | # but keep track of directories ending in .cache 184 | !*.[Cc]ache/ 185 | 186 | # Others 187 | ClientBin/ 188 | ~$* 189 | *~ 190 | *.dbmdl 191 | *.dbproj.schemaview 192 | *.jfm 193 | *.pfx 194 | *.publishsettings 195 | node_modules/ 196 | orleans.codegen.cs 197 | 198 | # Since there are multiple workflows, uncomment next line to ignore bower_components 199 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 200 | #bower_components/ 201 | 202 | # RIA/Silverlight projects 203 | Generated_Code/ 204 | 205 | # Backup & report files from converting an old project file 206 | # to a newer Visual Studio version. Backup files are not needed, 207 | # because we have git ;-) 208 | _UpgradeReport_Files/ 209 | Backup*/ 210 | UpgradeLog*.XML 211 | UpgradeLog*.htm 212 | 213 | # SQL Server files 214 | *.mdf 215 | *.ldf 216 | 217 | # Business Intelligence projects 218 | *.rdl.data 219 | *.bim.layout 220 | *.bim_*.settings 221 | 222 | # Microsoft Fakes 223 | FakesAssemblies/ 224 | 225 | # GhostDoc plugin setting file 226 | *.GhostDoc.xml 227 | 228 | # Node.js Tools for Visual Studio 229 | .ntvs_analysis.dat 230 | 231 | # Visual Studio 6 build log 232 | *.plg 233 | 234 | # Visual Studio 6 workspace options file 235 | *.opt 236 | 237 | # Visual Studio LightSwitch build output 238 | **/*.HTMLClient/GeneratedArtifacts 239 | **/*.DesktopClient/GeneratedArtifacts 240 | **/*.DesktopClient/ModelManifest.xml 241 | **/*.Server/GeneratedArtifacts 242 | **/*.Server/ModelManifest.xml 243 | _Pvt_Extensions 244 | 245 | # Paket dependency manager 246 | .paket/paket.exe 247 | paket-files/ 248 | 249 | # FAKE - F# Make 250 | .fake/ 251 | 252 | # JetBrains Rider 253 | .idea/ 254 | *.sln.iml 255 | 256 | # CodeRush 257 | .cr/ 258 | 259 | # Python Tools for Visual Studio (PTVS) 260 | __pycache__/ 261 | *.pyc 262 | 263 | .RHistory 264 | misc/ 265 | .Rproj.user 266 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | This project welcomes contributions and suggestions. Most contributions require you to agree to a 4 | Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us 5 | the rights to use your contribution. For details, visit https://cla.microsoft.com. 6 | 7 | When you submit a pull request, a CLA-bot will automatically determine whether you need to provide 8 | a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions 9 | provided by the bot. You will only need to do this once across all repos using our CLA. 10 | 11 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 12 | For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or 13 | contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 14 | -------------------------------------------------------------------------------- /DESCRIPTION: -------------------------------------------------------------------------------- 1 | Package: AzureAuth 2 | Title: Authentication Services for Azure Active Directory 3 | Version: 1.3.3 4 | Authors@R: c( 5 | person("Hong", "Ooi", , "hongooi73@gmail.com", role = c("aut", "cre")), 6 | person("Tyler", "Littlefield", role="ctb"), 7 | person("httr development team", role="ctb", comment="Original OAuth listener code"), 8 | person("Scott", "Holden", , role = "ctb", comment = "Advice on AAD authentication"), 9 | person("Chris", "Stone", , role = "ctb", comment = "Advice on AAD authentication"), 10 | person("Microsoft", role="cph") 11 | ) 12 | Description: Provides Azure Active Directory (AAD) authentication functionality for R users of Microsoft's 'Azure' cloud . Use this package to obtain 'OAuth' 2.0 tokens for services including Azure Resource Manager, Azure Storage and others. It supports both AAD v1.0 and v2.0, as well as multiple authentication methods, including device code and resource owner grant. Tokens are cached in a user-specific directory obtained using the 'rappdirs' package. The interface is based on the 'OAuth' framework in the 'httr' package, but customised and streamlined for Azure. Part of the 'AzureR' family of packages. 13 | URL: https://github.com/Azure/AzureAuth https://github.com/Azure/AzureR 14 | BugReports: https://github.com/Azure/AzureAuth/issues 15 | License: MIT + file LICENSE 16 | VignetteBuilder: knitr 17 | Depends: 18 | R (>= 3.3) 19 | Imports: 20 | utils, 21 | httr (>= 1.3), 22 | openssl, 23 | jsonlite, 24 | jose, 25 | R6, 26 | rappdirs 27 | Suggests: 28 | knitr, 29 | rmarkdown, 30 | testthat, 31 | httpuv, 32 | shiny, 33 | shinyjs, 34 | AzureRMR, 35 | AzureGraph 36 | Roxygen: list(markdown=TRUE, r6=FALSE) 37 | RoxygenNote: 7.1.1 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | YEAR: 2019-2021 2 | COPYRIGHT HOLDER: Microsoft 3 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | Copyright (c) 2019 Microsoft 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 | -------------------------------------------------------------------------------- /NAMESPACE: -------------------------------------------------------------------------------- 1 | # Generated by roxygen2: do not edit by hand 2 | 3 | S3method(decode_jwt,AzureToken) 4 | S3method(decode_jwt,Token) 5 | S3method(decode_jwt,character) 6 | S3method(extract_jwt,AzureToken) 7 | S3method(extract_jwt,Token) 8 | S3method(extract_jwt,character) 9 | export(AzureR_dir) 10 | export(AzureToken) 11 | export(AzureTokenAuthCode) 12 | export(AzureTokenClientCreds) 13 | export(AzureTokenDeviceCode) 14 | export(AzureTokenManaged) 15 | export(AzureTokenOnBehalfOf) 16 | export(AzureTokenResOwner) 17 | export(build_authorization_uri) 18 | export(cert_assertion) 19 | export(clean_token_directory) 20 | export(create_AzureR_dir) 21 | export(decode_jwt) 22 | export(delete_azure_token) 23 | export(extract_jwt) 24 | export(format_auth_header) 25 | export(get_azure_token) 26 | export(get_device_creds) 27 | export(get_managed_token) 28 | export(is_azure_token) 29 | export(is_azure_v1_token) 30 | export(is_azure_v2_token) 31 | export(is_guid) 32 | export(list_azure_tokens) 33 | export(load_azure_token) 34 | export(normalize_guid) 35 | export(normalize_tenant) 36 | export(token_hash) 37 | -------------------------------------------------------------------------------- /NEWS.md: -------------------------------------------------------------------------------- 1 | # AzureAuth 1.3.3 2 | 3 | - Documentation update only: 4 | - Clarify that you can use `get_managed_token` to obtain tokens with a user-defined identity, not just a system identity. 5 | - Clarify the distinction between authentication and authorization in the `get_azure_token` help, and also in the Shiny vignette. 6 | - Add a webapp (Shiny) scenario to the "Common authentication scenarios" vignette. 7 | 8 | # AzureAuth 1.3.2 9 | 10 | - Change the default caching behaviour to disable the cache if running inside Shiny. 11 | - Update Shiny vignette to clean up redirect page after authenticating (thanks to Tyler Littlefield). 12 | - Revert the changed behaviour for caching directory creation in 1.3.1. 13 | - Add a `create_AzureR_dir` function to create the caching directory manually. This can be useful not just for non-interactive sessions, but also Jupyter and R notebooks, which are not _technically_ interactive in the sense that they cannot read user input from a console prompt. 14 | 15 | # AzureAuth 1.3.1 16 | 17 | - Allow specifying the location of the token caching directory in the environment variable `R_AZURE_DATA_DIR`. 18 | - Change `clean_token_directory` to actually clean the directory (delete all files). This is because the main non-token objects found here are AzureRMR and AzureGraph logins, which are orphaned once their backing tokens are deleted. Deleting them as well is less confusing, as a message will be displayed saying to create a new login. 19 | - Always create the token caching directory, rather than asking first. This should result in consistent behaviour for both interactive and non-interactive sessions. 20 | - Add a vignette outlining the app registration settings and `get_azure_token` arguments for some common authentication scenarios. 21 | 22 | # AzureAuth 1.3.0 23 | 24 | - Allow obtaining tokens for the `organizations` and `consumers` generic tenants, in addition to `common`. 25 | - More robust handling of expiry time calculation for AAD v2.0 authentication. 26 | 27 | # AzureAuth 1.2.5 28 | 29 | - Change maintainer email address. 30 | 31 | # AzureAuth 1.2.4 32 | 33 | - Allow any scheme to be used in the URI for a token resource, not just HTTP\[S\]. 34 | - Documentation/vignette fixes. 35 | 36 | # AzureAuth 1.2.3 37 | 38 | * `is_guid`, `normalize_guid` and `normalize_tenant` now accept vector arguments. `normalize_guid` throws an error if any of its argument values is not a valid GUID. 39 | * `get_azure_token` will now display the authentication method it chooses if the `auth_type` argument is not explicitly specified. To avoid surprises, it's still recommended that you specify `auth_type` when obtaining a token. 40 | * New `load_azure_token` function to retrieve a token from the cache, given its hash value. 41 | * Fixes to allow authenticating personal accounts without a tenant. 42 | 43 | # AzureAuth 1.2.2 44 | 45 | * Only call `utils::askYesNo` if R version is 3.5 or higher. 46 | 47 | # AzureAuth 1.2.1 48 | 49 | * Pass the resource and scope as explicit parameters to the AAD endpoint when refreshing a token. Among other things, this allows using a refresh token from one resource to obtain an access token for another resource. 50 | * Use `utils::askYesNo` for prompts, eg when creating the AzureR caching directory and deleting tokens; this fixes a bug in reading the input. As a side-effect, Windows users who are using RGUI.exe will see a popup dialog box instead of a message in the terminal. 51 | 52 | # AzureAuth 1.2.0 53 | 54 | * Changes to token acquisition code to better integrate with Shiny. Use the `build_authorization_uri` and `get_device_creds` functions to initiate the authorization step from within a Shiny web app. `get_azure_token` has new `auth_code` and `device_creds` arguments for passing in authorization details obtained separately. See the "Authenticating from Shiny" vignette for a skeleton example app. 55 | * Add `use_cache` argument to `get_azure_token` and `get_managed_token`, which controls whether to cache tokens. Set this to FALSE to skip reading cached credentials from disk, and to skip saving credentials to the cache. 56 | * Make `decode_jwt` a generic, with methods for character strings, `AzureToken` objects and `httr::Token` objects. 57 | * Add `extract_jwt` generic to get the actual token from within an R object, with methods for character strings, `AzureToken` objects and `httr::Token` objects. 58 | * Fix bug in checking the expiry time for AAD v2.0 tokens. 59 | * Extend `get_managed_token` to work from within Azure Functions. 60 | * Refactor the underlying classes to represent authentication flows, which have a much greater impact on the program logic than AAD version. In place of `AzureTokenV1` and `AzureTokenV2` classes, there are now `AzureTokenAuthCode`, `AzureTokenDeviceCode`, `AzureTokenClientCreds`, `AzureTokenOnBehalfOf`, `AzureTokenResOwner`, and `AzureTokenManaged`. There should be no user-visible changes in behaviour arising from this. 61 | 62 | # AzureAuth 1.1.1 63 | 64 | * New `get_managed_token` function to obtain a token for a managed identity. Note this only works within a VM, service or container to which an identity has been assigned. 65 | 66 | # AzureAuth 1.1.0 67 | 68 | * Much improved support for authenticating with a certificate. In the `certificate` argument, specify either the name of a PEM/PFX file, or an AzureKeyVault object representing a cert. 69 | * Support providing a path in the `aad_host` argument, for Azure B2C logins. 70 | * Fix bug that prevented `token_args` argument from being passed to the token endpoint. 71 | * If authentication fails using the `authorization_code` flow, print the AAD error message, if possible. 72 | * Add support for the `on_behalf_of` authorization flow. 73 | 74 | # AzureAuth 1.0.2 75 | 76 | * Corrections to vignette and readme. 77 | * Make prompt to create caching directory more generic, since other AzureR packages will also use it. 78 | 79 | # AzureAuth 1.0.1 80 | 81 | * Export `decode_jwt`, a utility function to view the token data. 82 | * Force tokens to be cached using version 2 of the RDS format. This is mostly to ensure backward compatibility if the default format used by `saveRDS` ever changes. 83 | 84 | # AzureAuth 1.0.0 85 | 86 | * Submitted to CRAN 87 | -------------------------------------------------------------------------------- /R/AzureAuth.R: -------------------------------------------------------------------------------- 1 | utils::globalVariables(c("self", "private")) 2 | 3 | 4 | .onLoad <- function(libname, pkgname) 5 | { 6 | make_AzureR_dir() 7 | options(azure_imds_version="2018-02-01") 8 | invisible(NULL) 9 | } 10 | 11 | 12 | # create a directory for saving creds -- ask first, to satisfy CRAN requirements 13 | make_AzureR_dir <- function() 14 | { 15 | AzureR_dir <- AzureR_dir() 16 | if(!dir.exists(AzureR_dir) && interactive()) 17 | { 18 | ok <- get_confirmation(paste0( 19 | "The AzureR packages can save your authentication credentials in the directory:\n\n", 20 | AzureR_dir, "\n\n", 21 | "This saves you having to re-authenticate with Azure in future sessions. Create this directory?")) 22 | if(!ok) 23 | return(invisible(NULL)) 24 | 25 | dir.create(AzureR_dir, recursive=TRUE) 26 | } 27 | } 28 | 29 | 30 | #' Data directory for AzureR packages 31 | #' 32 | #' @details 33 | #' AzureAuth can save your authentication credentials in a user-specific directory, using the rappdirs package. On recent Windows versions, this will usually be in the location `C:\\Users\\(username)\\AppData\\Local\\AzureR`. On Unix/Linux, it will be in `~/.local/share/AzureR`, and on MacOS, it will be in `~/Library/Application Support/AzureR`.Alternatively, you can specify the location of the directory in the environment variable `R_AZURE_DATA_DIR`. AzureAuth does not modify R's working directory, which significantly lessens the risk of accidentally introducing cached tokens into source control. 34 | #' 35 | #' On package startup, if this directory does not exist, AzureAuth will prompt you for permission to create it. It's recommended that you allow the directory to be created, as otherwise you will have to reauthenticate with Azure every time. Note that many cloud engineering tools, including the [Azure CLI](https://docs.microsoft.com/en-us/cli/azure/?view=azure-cli-latest), save authentication credentials in this way. The prompt only appears in an interactive session (in the sense that `interactive()` returns TRUE); if AzureAuth is loaded in a batch script, the directory is not created if it doesn't already exist. 36 | #' 37 | #' `create_AzureR_dir` is a utility function to create the caching directory manually. This can be useful not just for non-interactive sessions, but also Jupyter and R notebooks, which are not _technically_ interactive in that `interactive()` returns FALSE. 38 | #' 39 | #' The caching directory is also used by other AzureR packages, notably AzureRMR (for storing Resource Manager logins) and AzureGraph (for Microsoft Graph logins). You should not save your own files in it; instead, treat it as something internal to the AzureR packages. 40 | #' 41 | #' @return 42 | #' A string containing the data directory. 43 | #' 44 | #' @seealso 45 | #' [get_azure_token] 46 | #' 47 | #' [rappdirs::user_data_dir] 48 | #' 49 | #' @rdname AzureR_dir 50 | #' @export 51 | AzureR_dir <- function() 52 | { 53 | userdir <- Sys.getenv("R_AZURE_DATA_DIR") 54 | if(userdir != "") 55 | return(userdir) 56 | rappdirs::user_data_dir(appname="AzureR", appauthor="", roaming=FALSE) 57 | } 58 | 59 | 60 | #' @rdname AzureR_dir 61 | #' @export 62 | create_AzureR_dir <- function() 63 | { 64 | azdir <- AzureR_dir() 65 | if(!dir.exists(azdir)) 66 | dir.create(azdir, recursive=TRUE) 67 | } 68 | -------------------------------------------------------------------------------- /R/AzureToken.R: -------------------------------------------------------------------------------- 1 | #' Azure OAuth authentication 2 | #' 3 | #' Azure OAuth 2.0 token classes, with an interface based on the [Token2.0 class][httr::Token2.0] in httr. Rather than calling the initialization methods directly, tokens should be created via [get_azure_token()]. 4 | #' 5 | #' @docType class 6 | #' @section Methods: 7 | #' - `refresh`: Refreshes the token. For expired tokens without an associated refresh token, refreshing really means requesting a new token. 8 | #' - `validate`: Checks if the token has not yet expired. Note that a token may be invalid for reasons other than having expired, eg if it is revoked on the server. 9 | #' - `hash`: Computes an MD5 hash on the input fields of the object. Used internally for identification purposes when caching. 10 | #' - `cache`: Stores the token on disk for use in future sessions. 11 | #' 12 | #' @seealso 13 | #' [get_azure_token], [httr::Token] 14 | #' 15 | #' @format An R6 object representing an Azure Active Directory token and its associated credentials. `AzureToken` is the base class, and the others inherit from it. 16 | #' @export 17 | AzureToken <- R6::R6Class("AzureToken", 18 | 19 | public=list( 20 | 21 | version=NULL, 22 | resource=NULL, 23 | scope=NULL, 24 | aad_host=NULL, 25 | tenant=NULL, 26 | auth_type=NULL, 27 | client=NULL, 28 | token_args=list(), 29 | authorize_args=list(), 30 | credentials=NULL, # returned token details from host 31 | 32 | initialize=function(resource, tenant, app, password=NULL, username=NULL, certificate=NULL, 33 | aad_host="https://login.microsoftonline.com/", version=1, 34 | authorize_args=list(), token_args=list(), 35 | use_cache=NULL, auth_info=NULL) 36 | { 37 | if(is.null(private$initfunc)) 38 | stop("Do not call this constructor directly; use get_azure_token() instead") 39 | 40 | self$version <- normalize_aad_version(version) 41 | if(self$version == 1) 42 | { 43 | if(length(resource) != 1) 44 | stop("Resource for Azure Active Directory v1.0 token must be a single string", call.=FALSE) 45 | self$resource <- resource 46 | } 47 | else self$scope <- sapply(resource, verify_v2_scope, USE.NAMES=FALSE) 48 | 49 | # default behaviour: disable cache if running in shiny 50 | if(is.null(use_cache)) 51 | use_cache <- !in_shiny() 52 | 53 | self$aad_host <- aad_host 54 | self$tenant <- normalize_tenant(tenant) 55 | self$token_args <- token_args 56 | private$use_cache <- use_cache 57 | 58 | # use_cache = NA means return dummy object: initialize fields, but don't contact AAD 59 | if(is.na(use_cache)) 60 | return() 61 | 62 | if(use_cache) 63 | private$load_cached_credentials() 64 | 65 | # time of initial request for token: in case we need to set expiry time manually 66 | request_time <- Sys.time() 67 | if(is.null(self$credentials)) 68 | { 69 | res <- private$initfunc(auth_info) 70 | self$credentials <- process_aad_response(res) 71 | } 72 | private$set_expiry_time(request_time) 73 | 74 | if(private$use_cache) 75 | self$cache() 76 | }, 77 | 78 | cache=function() 79 | { 80 | if(dir.exists(AzureR_dir())) 81 | { 82 | filename <- file.path(AzureR_dir(), self$hash()) 83 | saveRDS(self, filename, version=2) 84 | } 85 | invisible(NULL) 86 | }, 87 | 88 | hash=function() 89 | { 90 | token_hash_internal(self$version, self$aad_host, self$tenant, self$auth_type, self$client, 91 | self$resource, self$scope, self$authorize_args, self$token_args) 92 | }, 93 | 94 | validate=function() 95 | { 96 | if(is.null(self$credentials$expires_on) || is.na(self$credentials$expires_on)) 97 | return(TRUE) 98 | 99 | expdate <- as.POSIXct(as.numeric(self$credentials$expires_on), origin="1970-01-01") 100 | curdate <- Sys.time() 101 | curdate < expdate 102 | }, 103 | 104 | can_refresh=function() 105 | { 106 | TRUE 107 | }, 108 | 109 | refresh=function() 110 | { 111 | request_time <- Sys.time() 112 | res <- if(!is.null(self$credentials$refresh_token)) 113 | { 114 | body <- list(grant_type="refresh_token", 115 | client_id=self$client$client_id, 116 | client_secret=self$client$client_secret, 117 | resource=self$resource, 118 | scope=paste_v2_scopes(self$scope), 119 | client_assertion=self$client$client_assertion, 120 | client_assertion_type=self$client$client_assertion_type, 121 | refresh_token=self$credentials$refresh_token 122 | ) 123 | 124 | uri <- private$aad_uri("token") 125 | httr::POST(uri, body=body, encode="form") 126 | } 127 | else private$initfunc() # reauthenticate if no refresh token (cannot reuse any supplied creds) 128 | 129 | creds <- try(process_aad_response(res)) 130 | if(inherits(creds, "try-error")) 131 | { 132 | delete_azure_token(hash=self$hash(), confirm=FALSE) 133 | stop("Unable to refresh token", call.=FALSE) 134 | } 135 | 136 | self$credentials <- creds 137 | private$set_expiry_time(request_time) 138 | 139 | if(private$use_cache) 140 | self$cache() 141 | invisible(self) 142 | }, 143 | 144 | print=function() 145 | { 146 | cat(format_auth_header(self)) 147 | invisible(self) 148 | } 149 | ), 150 | 151 | private=list( 152 | 153 | use_cache=NULL, 154 | 155 | load_cached_credentials=function() 156 | { 157 | tokenfile <- file.path(AzureR_dir(), self$hash()) 158 | if(!file.exists(tokenfile)) 159 | return(NULL) 160 | 161 | message("Loading cached token") 162 | token <- readRDS(tokenfile) 163 | if(!is_azure_token(token)) 164 | { 165 | file.remove(tokenfile) 166 | stop("Invalid or corrupted cached token", call.=FALSE) 167 | } 168 | 169 | self$credentials <- token$credentials 170 | if(!self$validate()) 171 | self$refresh() 172 | }, 173 | 174 | set_expiry_time=function(request_time) 175 | { 176 | # v2.0 endpoint doesn't provide an expires_on field, set it here 177 | if(is.null(self$credentials$expires_on)) 178 | { 179 | expiry <- try(decode_jwt(self$credentials$access_token)$payload$exp, silent=TRUE) 180 | if(inherits(expiry, "try-error")) 181 | expiry <- try(decode_jwt(self$credentials$id_token)$payload$exp, silent=TRUE) 182 | if(inherits(expiry, "try-error")) 183 | expiry <- NA 184 | 185 | expires_in <- if(!is.null(self$credentials$expires_in)) 186 | as.numeric(self$credentials$expires_in) 187 | else NA 188 | 189 | request_time <- floor(as.numeric(request_time)) 190 | expires_on <- request_time + expires_in 191 | 192 | self$credentials$expires_on <- if(is.na(expiry) && is.na(expires_on)) 193 | { 194 | warning("Could not set expiry time, using default validity period of 1 hour") 195 | as.character(as.numeric(request_time + 3600)) 196 | } 197 | else as.character(as.numeric(min(expiry, expires_on, na.rm=TRUE))) 198 | } 199 | }, 200 | 201 | aad_uri=function(type, ...) 202 | { 203 | aad_uri(self$aad_host, self$tenant, self$version, type, list(...)) 204 | }, 205 | 206 | build_access_body=function(body=self$client) 207 | { 208 | stopifnot(is.list(self$token_args)) 209 | 210 | # fill in cert assertion details 211 | body$client_assertion <- build_assertion(body$client_assertion, 212 | self$tenant, body$client_id, self$aad_host, self$version) 213 | 214 | c(body, self$token_args, 215 | if(self$version == 1) 216 | list(resource=self$resource) 217 | else list(scope=paste_v2_scopes(self$scope)) 218 | ) 219 | } 220 | )) 221 | 222 | -------------------------------------------------------------------------------- /R/cert_creds.R: -------------------------------------------------------------------------------- 1 | #' Create a client assertion for certificate authentication 2 | #' 3 | #' @param certificate An Azure Key Vault certificate object, or the name of a PEM or PFX file containing _both_ a private key and a public certificate. 4 | #' @param duration The requested validity period of the token, in seconds. The default is 1 hour. 5 | #' @param signature_size The size of the SHA2 signature. 6 | #' @param ... Other named arguments which will be treated as custom claims. 7 | #' 8 | #' @details 9 | #' Use this function to customise a client assertion for authenticating with a certificate. 10 | #' 11 | #' @return 12 | #' An object of S3 class `cert_assertion`, which is a list representing the assertion. 13 | #' 14 | #' @seealso 15 | #' [get_azure_token] 16 | #' 17 | #' @examples 18 | #' \dontrun{ 19 | #' 20 | #' cert_assertion("mycert.pem", duration=2*3600) 21 | #' cert_assertion("mycert.pem", custom_data="some text") 22 | #' 23 | #' # using a cert stored in Azure Key Vault 24 | #' cert <- AzureKeyVault::key_vault("myvault")$certificates$get("mycert") 25 | #' cert_assertion(cert, duration=2*3600) 26 | #' 27 | #' } 28 | #' @export 29 | cert_assertion <- function(certificate, duration=3600, signature_size=256, ...) 30 | { 31 | structure(list(cert=certificate, duration=duration, size=signature_size, claims=list(...)), 32 | class="cert_assertion") 33 | } 34 | 35 | 36 | build_assertion <- function(assertion, ...) 37 | { 38 | UseMethod("build_assertion") 39 | } 40 | 41 | 42 | build_assertion.stored_cert <- function(assertion, ...) 43 | { 44 | build_assertion(cert_assertion(assertion), ...) 45 | } 46 | 47 | 48 | build_assertion.character <- function(assertion, ...) 49 | { 50 | pair <- read_cert_pair(assertion) 51 | build_assertion(cert_assertion(pair), ...) 52 | } 53 | 54 | 55 | build_assertion.cert_assertion <- function(assertion, tenant, app, aad_host, version) 56 | { 57 | url <- httr::parse_url(aad_host) 58 | if(url$path == "") 59 | { 60 | url$path <- if(version == 1) 61 | file.path(tenant, "oauth2/token") 62 | else file.path(tenant, "oauth2/v2.0/token") 63 | } 64 | 65 | claim <- jose::jwt_claim(iss=app, sub=app, aud=httr::build_url(url), 66 | exp=as.numeric(Sys.time() + assertion$duration)) 67 | 68 | if(!is_empty(assertion$claims)) 69 | claim <- utils::modifyList(claim, assertion$claims) 70 | 71 | sign_assertion(assertion$cert, claim, assertion$size) 72 | } 73 | 74 | 75 | build_assertion.default <- function(assertion, ...) 76 | { 77 | if(is.null(assertion)) 78 | assertion 79 | else stop("Invalid certificate assertion", call.=FALSE) 80 | } 81 | 82 | 83 | sign_assertion <- function(certificate, claim, size) 84 | { 85 | UseMethod("sign_assertion") 86 | } 87 | 88 | 89 | sign_assertion.stored_cert <- function(certificate, claim, size) 90 | { 91 | kty <- certificate$policy$key_props$kty # key type determines signing alg 92 | alg <- if(kty == "RSA") 93 | paste0("RS", size) 94 | else paste0("ES", size) 95 | 96 | header <- list(alg=alg, x5t=certificate$x5t, kid=certificate$x5t, typ="JWT") 97 | token_conts <- paste(token_encode(header), token_encode(claim), sep=".") 98 | 99 | paste(token_conts, certificate$sign(openssl::sha2(charToRaw(token_conts), size=size), alg), sep=".") 100 | } 101 | 102 | 103 | sign_assertion.openssl_cert_pair <- function(certificate, claim, size) 104 | { 105 | alg <- if(inherits(certificate$key, "rsa")) 106 | paste0("RS", size) 107 | else if(inherits(certificate$key, "ecdsa")) 108 | paste0("EC", size) 109 | else stop("Unsupported key type", call.=FALSE) 110 | 111 | x5t <- jose::base64url_encode(openssl::sha1(certificate$cert)) 112 | header <- list(x5t=x5t, kid=x5t) 113 | 114 | jose::jwt_encode_sig(claim, certificate$key, size=size, header=header) 115 | } 116 | 117 | 118 | sign_assertion.character <- function(certificate, claim, size) 119 | { 120 | pair <- read_cert_pair(certificate) 121 | sign_assertion(pair, claim, size) 122 | } 123 | 124 | 125 | read_cert_pair <- function(file) 126 | { 127 | ext <- tolower(tools::file_ext(file)) 128 | if(ext == "pem") 129 | { 130 | pem <- openssl::read_pem(file) 131 | obj <- list( 132 | key=openssl::read_key(pem[["PRIVATE KEY"]]), 133 | cert=openssl::read_cert(pem[["CERTIFICATE"]]) 134 | ) 135 | } 136 | else if(ext %in% c("p12", "pfx")) 137 | { 138 | pfx <- openssl::read_p12(file) 139 | obj <- list(key=pfx$key, cert=pfx$cert) 140 | } 141 | else stop("Unsupported file extension: ", ext, call.=FALSE) 142 | 143 | structure(obj, class="openssl_cert_pair") 144 | } 145 | 146 | 147 | token_encode <- function(x) 148 | { 149 | jose::base64url_encode(jsonlite::toJSON(x, auto_unbox=TRUE)) 150 | } 151 | -------------------------------------------------------------------------------- /R/classes.R: -------------------------------------------------------------------------------- 1 | #' @rdname AzureToken 2 | #' @export 3 | AzureTokenAuthCode <- R6::R6Class("AzureTokenAuthCode", inherit=AzureToken, 4 | 5 | public=list( 6 | 7 | initialize=function(common_args, authorize_args, auth_code) 8 | { 9 | self$auth_type <- "authorization_code" 10 | self$authorize_args <- authorize_args 11 | with(common_args, 12 | private$set_request_credentials(app, password, username)) 13 | do.call(super$initialize, c(common_args, list(auth_info=auth_code))) 14 | 15 | # notify user if no refresh token 16 | if(!is.null(self$credentials) && is.null(self$credentials$refresh_token)) 17 | norenew_alert(self$version) 18 | } 19 | ), 20 | 21 | private=list( 22 | 23 | initfunc=function(code=NULL) 24 | { 25 | stopifnot(is.list(self$token_args)) 26 | stopifnot(is.list(self$authorize_args)) 27 | 28 | opts <- utils::modifyList(list( 29 | resource=if(self$version == 1) self$resource else self$scope, 30 | tenant=self$tenant, 31 | app=self$client$client_id, 32 | username=self$client$login_hint, 33 | aad_host=self$aad_host, 34 | version=self$version 35 | ), self$authorize_args) 36 | 37 | auth_uri <- do.call(build_authorization_uri, opts) 38 | redirect <- httr::parse_url(auth_uri)$query$redirect_uri 39 | 40 | if(is.null(code)) 41 | { 42 | if(!requireNamespace("httpuv", quietly=TRUE)) 43 | stop("httpuv package must be installed to use authorization_code method", call.=FALSE) 44 | 45 | code <- listen_for_authcode(auth_uri, redirect) 46 | } 47 | 48 | # contact token endpoint for token 49 | access_uri <- private$aad_uri("token") 50 | body <- c(self$client, code=code, redirect_uri=redirect, self$token_args) 51 | 52 | httr::POST(access_uri, body=body, encode="form") 53 | }, 54 | 55 | set_request_credentials=function(app, password, username) 56 | { 57 | object <- list(client_id=app, grant_type="authorization_code") 58 | 59 | if(!is.null(username)) 60 | object$login_hint <- username 61 | if(!is.null(password)) 62 | object$client_secret <- password 63 | 64 | self$client <- object 65 | } 66 | )) 67 | 68 | 69 | #' @rdname AzureToken 70 | #' @export 71 | AzureTokenDeviceCode <- R6::R6Class("AzureTokenDeviceCode", inherit=AzureToken, 72 | 73 | public=list( 74 | 75 | initialize=function(common_args, device_creds) 76 | { 77 | self$auth_type <- "device_code" 78 | with(common_args, 79 | private$set_request_credentials(app)) 80 | do.call(super$initialize, c(common_args, list(auth_info=device_creds))) 81 | 82 | # notify user if no refresh token 83 | if(!is.null(self$credentials) && is.null(self$credentials$refresh_token)) 84 | norenew_alert(self$version) 85 | } 86 | ), 87 | 88 | private=list( 89 | 90 | initfunc=function(creds=NULL) 91 | { 92 | if(is.null(creds)) 93 | { 94 | creds <- get_device_creds( 95 | if(self$version == 1) self$resource else self$scope, 96 | tenant=self$tenant, 97 | app=self$client$client_id, 98 | aad_host=self$aad_host, 99 | version=self$version 100 | ) 101 | cat(creds$message, "\n") 102 | } 103 | 104 | # poll token endpoint for token 105 | access_uri <- private$aad_uri("token") 106 | body <- c(self$client, code=creds$device_code) 107 | 108 | poll_for_token(access_uri, body, creds$interval, creds$expires_in) 109 | }, 110 | 111 | set_request_credentials=function(app) 112 | { 113 | self$client <- list(client_id=app, grant_type="device_code") 114 | } 115 | )) 116 | 117 | 118 | #' @rdname AzureToken 119 | #' @export 120 | AzureTokenClientCreds <- R6::R6Class("AzureTokenClientCreds", inherit=AzureToken, 121 | 122 | public=list( 123 | 124 | initialize=function(common_args) 125 | { 126 | self$auth_type <- "client_credentials" 127 | with(common_args, 128 | private$set_request_credentials(app, password, certificate)) 129 | do.call(super$initialize, common_args) 130 | } 131 | ), 132 | 133 | private=list( 134 | 135 | initfunc=function(init_args) 136 | { 137 | # contact token endpoint directly with client credentials 138 | uri <- private$aad_uri("token") 139 | body <- private$build_access_body() 140 | 141 | httr::POST(uri, body=body, encode="form") 142 | }, 143 | 144 | set_request_credentials=function(app, password, certificate) 145 | { 146 | object <- list(client_id=app, grant_type="client_credentials") 147 | 148 | if(!is.null(password)) 149 | object$client_secret <- password 150 | else if(!is.null(certificate)) 151 | { 152 | object$client_assertion_type <- "urn:ietf:params:oauth:client-assertion-type:jwt-bearer" 153 | object$client_assertion <- certificate # not actual assertion: will be replaced later 154 | } 155 | else stop("Must provide either a client secret or certificate for client_credentials grant", 156 | call.=FALSE) 157 | 158 | self$client <- object 159 | } 160 | )) 161 | 162 | 163 | #' @rdname AzureToken 164 | #' @export 165 | AzureTokenOnBehalfOf <- R6::R6Class("AzureTokenOnBehalfOf", inherit=AzureToken, 166 | 167 | public=list( 168 | 169 | initialize=function(common_args, on_behalf_of) 170 | { 171 | self$auth_type <- "on_behalf_of" 172 | with(common_args, 173 | private$set_request_credentials(app, password, certificate, on_behalf_of)) 174 | do.call(super$initialize, common_args) 175 | } 176 | ), 177 | 178 | private=list( 179 | 180 | initfunc=function(init_args) 181 | { 182 | # contact token endpoint directly with client credentials 183 | uri <- private$aad_uri("token") 184 | body <- private$build_access_body() 185 | 186 | httr::POST(uri, body=body, encode="form") 187 | }, 188 | 189 | set_request_credentials=function(app, password, certificate, on_behalf_of) 190 | { 191 | if(is_empty(on_behalf_of)) 192 | stop("Must provide an Azure token for on_behalf_of grant", call.=FALSE) 193 | 194 | object <- list(client_id=app, grant_type="urn:ietf:params:oauth:grant-type:jwt-bearer") 195 | 196 | if(!is.null(password)) 197 | object$client_secret <- password 198 | else if(!is.null(certificate)) 199 | { 200 | object$client_assertion_type <- "urn:ietf:params:oauth:client-assertion-type:jwt-bearer" 201 | object$client_assertion <- certificate # not actual assertion: will be replaced later 202 | } 203 | else stop("Must provide either a client secret or certificate for on_behalf_of grant", 204 | call.=FALSE) 205 | 206 | object$requested_token_use <- "on_behalf_of" 207 | object$assertion <- extract_jwt(on_behalf_of) 208 | 209 | self$client <- object 210 | } 211 | )) 212 | 213 | 214 | #' @rdname AzureToken 215 | #' @export 216 | AzureTokenResOwner <- R6::R6Class("AzureTokenResOwner", inherit=AzureToken, 217 | 218 | public=list( 219 | 220 | initialize=function(common_args) 221 | { 222 | self$auth_type <- "resource_owner" 223 | with(common_args, 224 | private$set_request_credentials(app, password, username)) 225 | do.call(super$initialize, common_args) 226 | } 227 | ), 228 | 229 | private=list( 230 | 231 | initfunc=function(init_args) 232 | { 233 | # contact token endpoint directly with resource owner username/password 234 | uri <- private$aad_uri("token") 235 | body <- private$build_access_body() 236 | 237 | httr::POST(uri, body=body, encode="form") 238 | }, 239 | 240 | set_request_credentials=function(app, password, username) 241 | { 242 | object <- list(client_id=app, grant_type="password") 243 | 244 | if(is.null(username) && is.null(password)) 245 | stop("Must provide a username and password for resource_owner grant", call.=FALSE) 246 | 247 | object$username <- username 248 | object$password <- password 249 | 250 | self$client <- object 251 | } 252 | )) 253 | 254 | 255 | #' @rdname AzureToken 256 | #' @export 257 | AzureTokenManaged <- R6::R6Class("AzureTokenManaged", inherit=AzureToken, 258 | 259 | public=list( 260 | 261 | initialize=function(resource, aad_host, token_args, use_cache) 262 | { 263 | self$auth_type <- "managed" 264 | super$initialize(resource, tenant="common", aad_host=aad_host, token_args=token_args, use_cache=use_cache) 265 | } 266 | ), 267 | 268 | private=list( 269 | 270 | initfunc=function(init_args) 271 | { 272 | stopifnot(is.list(self$token_args)) 273 | 274 | uri <- private$aad_uri("token") 275 | query <- utils::modifyList(self$token_args, 276 | list(`api-version`=getOption("azure_imds_version"), resource=self$resource)) 277 | 278 | secret <- Sys.getenv("MSI_SECRET") 279 | headers <- if(secret != "") 280 | httr::add_headers(secret=secret) 281 | else httr::add_headers(metadata="true") 282 | 283 | httr::GET(uri, headers, query=query) 284 | } 285 | )) 286 | 287 | 288 | norenew_alert <- function(version) 289 | { 290 | if(version == 1) 291 | message("Server did not provide a refresh token: please reauthenticate to refresh.") 292 | else message("Server did not provide a refresh token: you will have to reauthenticate to refresh.\n", 293 | "Add the 'offline_access' scope to obtain a refresh token.") 294 | } 295 | -------------------------------------------------------------------------------- /R/flow_init.R: -------------------------------------------------------------------------------- 1 | #' Standalone OAuth authorization functions 2 | #' 3 | #' @param resource,tenant,app,aad_host,version See the corresponding arguments for [get_azure_token]. 4 | #' @param username For `build_authorization_uri`, an optional login hint to be sent to the authorization endpoint. 5 | #' @param ... Named arguments that will be added to the authorization URI as query parameters. 6 | #' 7 | #' @details 8 | #' These functions are mainly for use in embedded scenarios, such as within a Shiny web app. In this case, the interactive authentication flows (authorization code and device code) need to be split up so that the authorization step is handled separately from the token acquisition step. You should not need to use these functions inside a regular R session, or when executing an R batch script. 9 | #' 10 | #' @return 11 | #' For `build_authorization_uri`, the authorization URI as a string. This can be set as a redirect from within a Shiny app's UI component. 12 | #' 13 | #' For `get_device_creds`, a list containing the following components: 14 | #' - `user_code`: A short string to be shown to the user 15 | #' - `device_code`: A long string to verify the session with the AAD server 16 | #' - `verification_uri`: The URI the user should browse to in order to login 17 | #' - `expires_in`: The duration in seconds for which the user and device codes are valid 18 | #' - `interval`: The interval between polling requests to the AAD token endpoint 19 | #' - `message`: A string with login instructions for the user 20 | #' 21 | #' @examples 22 | #' build_authorization_uri("https://myresource", "mytenant", "app_id", 23 | #' redirect_uri="http://localhost:8100") 24 | #' 25 | #' \dontrun{ 26 | #' 27 | #' ## obtaining an authorization code separately to acquiring the token 28 | #' # first, get the authorization URI 29 | #' auth_uri <- build_authorization_uri("https://management.azure.com/", "mytenant", "app_id") 30 | #' # browsing to the URI will log you in and redirect to another URI containing the auth code 31 | #' browseURL(auth_uri) 32 | #' # use the code to acquire the token 33 | #' get_azure_token("https://management.azure.com/", "mytenant", "app_id", 34 | #' auth_code="code-from-redirect") 35 | #' 36 | #' 37 | #' ## obtaining device credentials separately to acquiring the token 38 | #' # first, contact the authorization endpoint to get the user and device codes 39 | #' creds <- get_device_creds("https://management.azure.com/", "mytenant", "app_id") 40 | #' # print the login instructions 41 | #' creds$message 42 | #' # use the creds to acquire the token 43 | #' get_azure_token("https://management.azure.com/", "mytenant", "app_id", 44 | #' auth_type="device_code", device_creds=creds) 45 | #' 46 | #' } 47 | #' @rdname authorization 48 | #' @export 49 | build_authorization_uri <- function(resource, tenant, app, username=NULL, ..., 50 | aad_host="https://login.microsoftonline.com/", version=1) 51 | { 52 | version <- normalize_aad_version(version) 53 | default_opts <- list( 54 | client_id=app, 55 | response_type="code", 56 | redirect_uri="http://localhost:1410/", 57 | login_hint=username, 58 | state=paste0(sample(letters, 20, TRUE), collapse="") # random nonce 59 | ) 60 | default_opts <- if(version == 1) 61 | c(default_opts, resource=resource) 62 | else c(default_opts, scope=paste_v2_scopes(resource)) 63 | 64 | opts <- utils::modifyList(default_opts, list(...)) 65 | 66 | aad_uri(aad_host, normalize_tenant(tenant), version, "authorize", opts) 67 | } 68 | 69 | 70 | #' @rdname authorization 71 | #' @export 72 | get_device_creds <- function(resource, tenant, app, aad_host="https://login.microsoftonline.com/", version=1) 73 | { 74 | version <- normalize_aad_version(version) 75 | uri <- aad_uri(aad_host, normalize_tenant(tenant), version, "devicecode") 76 | body <- if(version == 1) 77 | list(resource=resource) 78 | else list(scope=paste_v2_scopes(resource)) 79 | body <- c(body, client_id=app) 80 | 81 | res <- httr::POST(uri, body=body, encode="form") 82 | process_aad_response(res) 83 | } 84 | -------------------------------------------------------------------------------- /R/format.R: -------------------------------------------------------------------------------- 1 | #' Format an AzureToken object 2 | #' 3 | #' @param token An Azure OAuth token. 4 | #' 5 | #' @rdname format 6 | #' @export 7 | format_auth_header <- function(token) 8 | { 9 | stopifnot(is_azure_token(token)) 10 | expiry <- as.POSIXct(as.numeric(token$credentials$expires_on), origin="1970-01-01") 11 | obtained <- expiry - as.numeric(token$credentials$expires_in) 12 | 13 | if(is_azure_v1_token(token)) 14 | { 15 | version <- "v1.0" 16 | res <- paste("resource", token$resource) 17 | } 18 | else 19 | { 20 | version <- "v2.0" 21 | res <- paste("scope", paste_v2_scopes(token$scope)) 22 | } 23 | 24 | tenant <- token$tenant 25 | if(tenant == "common") 26 | { 27 | token_obj <- try(decode_jwt(token), silent=TRUE) 28 | if(inherits(token_obj, "try-error")) 29 | { 30 | token_obj <- try(decode_jwt(token, "id"), silent=TRUE) 31 | if(inherits(token_obj, "try-error")) 32 | tenant <- "NA" 33 | else tenant <- paste0(tenant, " / ", token_obj$payload$tid) 34 | } 35 | else tenant <- paste0(tenant, " / ", token_obj$payload$tid) 36 | } 37 | 38 | paste0("Azure Active Directory ", version, " token for ", res, "\n", 39 | " Tenant: ", tenant, "\n", 40 | " App ID: ", token$client$client_id, "\n", 41 | " Authentication method: ", token$auth_type, "\n", 42 | " Token valid from: ", format(obtained, usetz=TRUE), " to: ", format(expiry, usetz=TRUE), "\n", 43 | " MD5 hash of inputs: ", token$hash(), "\n") 44 | } 45 | -------------------------------------------------------------------------------- /R/initfuncs.R: -------------------------------------------------------------------------------- 1 | listen_for_authcode <- function(remote_url, local_url) 2 | { 3 | local_url <- httr::parse_url(local_url) 4 | localhost <- if(local_url$hostname == "localhost") "127.0.0.1" else local_url$hostname 5 | localport <- local_url$port 6 | 7 | # based on httr::oauth_listener 8 | info <- NULL 9 | listen <- function(env) 10 | { 11 | query <- env$QUERY_STRING 12 | info <<- if(is.character(query) && nchar(query) > 0) 13 | httr::parse_url(query)$query 14 | else list() 15 | 16 | if(is_empty(info$code)) 17 | list(status=404L, headers=list(`Content-Type`="text/plain"), body="Not found") 18 | else list(status=200L, headers=list(`Content-Type`="text/plain"), 19 | body="Authenticated with Azure Active Directory. Please close this page and return to R.") 20 | } 21 | 22 | server <- httpuv::startServer(as.character(localhost), as.integer(localport), list(call=listen)) 23 | on.exit(httpuv::stopServer(server)) 24 | 25 | message("Waiting for authentication in browser...\nPress Esc/Ctrl + C to abort") 26 | httr::BROWSE(remote_url) 27 | 28 | while(is.null(info)) 29 | { 30 | httpuv::service() 31 | Sys.sleep(0.001) 32 | } 33 | httpuv::service() 34 | 35 | if(is_empty(info$code)) 36 | { 37 | msg <- gsub("\\+", " ", utils::URLdecode(info$error_description)) 38 | stop("Authentication failed. Message:\n", msg, call.=FALSE) 39 | } 40 | 41 | message("Authentication complete.") 42 | info$code 43 | } 44 | 45 | 46 | poll_for_token <- function(url, body, interval, period) 47 | { 48 | interval <- as.numeric(interval) 49 | ntries <- as.numeric(period) %/% interval 50 | body$grant_type <- "urn:ietf:params:oauth:grant-type:device_code" 51 | 52 | message("Waiting for device code in browser...\nPress Esc/Ctrl + C to abort") 53 | for(i in seq_len(ntries)) 54 | { 55 | Sys.sleep(interval) 56 | 57 | res <- httr::POST(url, body=body, encode="form") 58 | 59 | status <- httr::status_code(res) 60 | cont <- httr::content(res) 61 | if(status == 400 && cont$error == "authorization_pending") 62 | { 63 | # do nothing 64 | } 65 | else if(status >= 300) 66 | process_aad_response(res) # fail here on error 67 | else break 68 | } 69 | if(status >= 300) 70 | stop("Authentication failed.", call.=FALSE) 71 | 72 | message("Authentication complete.") 73 | res 74 | } 75 | 76 | -------------------------------------------------------------------------------- /R/jwt.R: -------------------------------------------------------------------------------- 1 | #' Get raw access token (which is a JWT object) 2 | #' 3 | #' @param token A token object. This can be an object of class `AzureToken`, of class `httr::Token`, or a character string containing the encoded token. 4 | #' @param type For the `AzureToken` and `httr::Token` methods, the token to decode/retrieve: either the access token or ID token. 5 | #' @param ... Other arguments passed to methods. 6 | #' 7 | #' @details 8 | #' An OAuth token is a _JSON Web Token_, which is a set of base64URL-encoded JSON objects containing the token credentials along with an optional (opaque) verification signature. `decode_jwt` decodes the credentials into an R object so they can be viewed. `extract_jwt` extracts the credentials from an R object of class `AzureToken` or `httr::Token`. 9 | #' 10 | #' Note that `decode_jwt` does not touch the token signature or attempt to verify the credentials. You should not rely on the decoded information without verifying it independently. Passing the token itself to Azure is safe, as Azure will carry out its own verification procedure. 11 | #' 12 | #' @return 13 | #' For `extract_jwt`, the character string containing the encoded token, suitable for including in a HTTP query. For `decode_jwt`, a list containing up to 3 components: `header`, `payload` and `signature`. 14 | #' 15 | #' @seealso 16 | #' [jwt.io](https://jwt.io), the main JWT informational site 17 | #' 18 | #' [jwt.ms](https://jwt.ms), Microsoft site to decode and explain JWTs 19 | #' 20 | #' [JWT Wikipedia entry](https://en.wikipedia.org/wiki/JSON_Web_Token) 21 | #' @rdname jwt 22 | #' @export 23 | decode_jwt <- function(token, ...) 24 | { 25 | UseMethod("decode_jwt") 26 | } 27 | 28 | 29 | #' @rdname jwt 30 | #' @export 31 | decode_jwt.AzureToken <- function(token, type=c("access", "id"), ...) 32 | { 33 | type <- paste0(match.arg(type), "_token") 34 | if(is.null(token$credentials[[type]])) 35 | stop(type, " not found", call.=FALSE) 36 | decode_jwt(token$credentials[[type]]) 37 | } 38 | 39 | 40 | #' @rdname jwt 41 | #' @export 42 | decode_jwt.Token <- function(token, type=c("access", "id"), ...) 43 | { 44 | type <- paste0(match.arg(type), "_token") 45 | if(is.null(token$credentials[[type]])) 46 | stop(type, " not found", call.=FALSE) 47 | decode_jwt(token$credentials[[type]]) 48 | } 49 | 50 | 51 | #' @rdname jwt 52 | #' @export 53 | decode_jwt.character <- function(token, ...) 54 | { 55 | token <- as.list(strsplit(token, "\\.")[[1]]) 56 | token[1:2] <- lapply(token[1:2], function(x) 57 | jsonlite::fromJSON(rawToChar(jose::base64url_decode(x)))) 58 | 59 | names(token)[1:2] <- c("header", "payload") 60 | if(length(token) > 2) 61 | names(token)[3] <- "signature" 62 | 63 | token 64 | } 65 | 66 | 67 | #' @rdname jwt 68 | #' @export 69 | extract_jwt <- function(token, ...) 70 | { 71 | UseMethod("extract_jwt") 72 | } 73 | 74 | 75 | #' @rdname jwt 76 | #' @export 77 | extract_jwt.AzureToken <- function(token, type=c("access", "id"), ...) 78 | { 79 | type <- match.arg(type) 80 | token$credentials[[paste0(type, "_token")]] 81 | } 82 | 83 | 84 | #' @rdname jwt 85 | #' @export 86 | extract_jwt.Token <- function(token, type=c("access", "id"), ...) 87 | { 88 | type <- match.arg(type) 89 | token$credentials[[paste0(type, "_token")]] 90 | } 91 | 92 | 93 | #' @rdname jwt 94 | #' @export 95 | extract_jwt.character <- function(token, ...) 96 | { 97 | token 98 | } 99 | 100 | -------------------------------------------------------------------------------- /R/managed_token.R: -------------------------------------------------------------------------------- 1 | #' @rdname get_azure_token 2 | #' @export 3 | get_managed_token <- function(resource, token_args=list(), use_cache=NULL) 4 | { 5 | aad_host <- Sys.getenv("MSI_ENDPOINT", "http://169.254.169.254/metadata/identity/oauth2") 6 | AzureTokenManaged$new(resource, aad_host, token_args=token_args, use_cache=use_cache) 7 | } 8 | -------------------------------------------------------------------------------- /R/normalize.R: -------------------------------------------------------------------------------- 1 | #' Normalize GUID and tenant values 2 | #' 3 | #' These functions are used by `get_azure_token` to recognise and properly format tenant and app IDs. `is_guid` can also be used generically for identifying GUIDs/UUIDs in any context. 4 | #' 5 | #' @param tenant For `normalize_tenant`, a string containing an Azure Active Directory tenant. This can be a name ("myaadtenant"), a fully qualified domain name ("myaadtenant.onmicrosoft.com" or "mycompanyname.com"), or a valid GUID. 6 | #' @param x For `is_guid`, a character string; for `normalize_guid`, a string containing a _validly formatted_ GUID. 7 | #' 8 | #' @details 9 | #' A tenant can be identified either by a GUID, or its name, or a fully-qualified domain name (FQDN). The rules for normalizing a tenant are: 10 | #' 1. If `tenant` is recognised as a valid GUID, return its canonically formatted value 11 | #' 2. Otherwise, if it is a FQDN, return it 12 | #' 3. Otherwise, if it is one of the generic tenants "common", "organizations" or "consumers", return it 13 | #' 4. Otherwise, append ".onmicrosoft.com" to it 14 | #' 15 | #' These functions are vectorised. See the link below for the GUID formats they accept. 16 | #' 17 | #' @return 18 | #' For `is_guid`, a logical vector indicating which values of `x` are validly formatted GUIDs. 19 | #' 20 | #' For `normalize_guid`, a vector of GUIDs in canonical format. If any values of `x` are not recognised as GUIDs, it throws an error. 21 | #' 22 | #' For `normalize_tenant`, the normalized tenant IDs or names. 23 | #' 24 | #' @seealso 25 | #' [get_azure_token] 26 | #' 27 | #' [Parsing rules for GUIDs in .NET](https://docs.microsoft.com/en-us/dotnet/api/system.guid.parse). `is_guid` and `normalize_guid` recognise the "N", "D", "B" and "P" formats. 28 | #' 29 | #' @examples 30 | #' 31 | #' is_guid("72f988bf-86f1-41af-91ab-2d7cd011db47") # TRUE 32 | #' is_guid("{72f988bf-86f1-41af-91ab-2d7cd011db47}") # TRUE 33 | #' is_guid("72f988bf-86f1-41af-91ab-2d7cd011db47}") # FALSE (unmatched brace) 34 | #' is_guid("microsoft") # FALSE 35 | #' 36 | #' # all of these return the same value 37 | #' normalize_guid("72f988bf-86f1-41af-91ab-2d7cd011db47") 38 | #' normalize_guid("{72f988bf-86f1-41af-91ab-2d7cd011db47}") 39 | #' normalize_guid("(72f988bf-86f1-41af-91ab-2d7cd011db47)") 40 | #' normalize_guid("72f988bf86f141af91ab2d7cd011db47") 41 | #' 42 | #' normalize_tenant("microsoft") # returns 'microsoft.onmicrosoft.com' 43 | #' normalize_tenant("microsoft.com") # returns 'microsoft.com' 44 | #' normalize_tenant("72f988bf-86f1-41af-91ab-2d7cd011db47") # returns the GUID 45 | #' 46 | #' # vector arguments are accepted 47 | #' ids <- c("72f988bf-86f1-41af-91ab-2d7cd011db47", "72f988bf86f141af91ab2d7cd011db47") 48 | #' is_guid(ids) 49 | #' normalize_guid(ids) 50 | #' normalize_tenant(c("microsoft", ids)) 51 | #' 52 | #' @export 53 | #' @rdname guid 54 | normalize_tenant <- function(tenant) 55 | { 56 | if(!is.character(tenant)) 57 | stop("Tenant must be a character string", call.=FALSE) 58 | 59 | tenant <- tolower(tenant) 60 | 61 | # check if supplied a guid; if not, check if a fqdn; 62 | # if not, check if 'common', 'organizations' or 'consumers'; if not, append '.onmicrosoft.com' 63 | guid <- is_guid(tenant) 64 | tenant[guid] <- normalize_guid(tenant[guid]) 65 | 66 | name <- !guid & !(tenant %in% c("common", "organizations", "consumers")) & !grepl(".", tenant, fixed=TRUE) 67 | tenant[name] <- paste0(tenant[name], ".onmicrosoft.com") 68 | 69 | tenant 70 | } 71 | 72 | 73 | #' @export 74 | #' @rdname guid 75 | normalize_guid <- function(x) 76 | { 77 | if(!all(is_guid(x))) 78 | stop("Not a GUID", call.=FALSE) 79 | 80 | x <- sub("^[({]?([-0-9a-f]+)[})]$", "\\1", x) 81 | x <- gsub("-", "", x) 82 | return(paste( 83 | substr(x, 1, 8), 84 | substr(x, 9, 12), 85 | substr(x, 13, 16), 86 | substr(x, 17, 20), 87 | substr(x, 21, 32), sep="-")) 88 | } 89 | 90 | 91 | #' @export 92 | #' @rdname guid 93 | is_guid <- function(x) 94 | { 95 | if(!is.character(x)) 96 | return(FALSE) 97 | x <- tolower(x) 98 | 99 | grepl("^[0-9a-f]{32}$", x) | 100 | grepl("^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$", x) | 101 | grepl("^\\{[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}}$", x) | 102 | grepl("^\\([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\\)$", x) 103 | } 104 | 105 | 106 | normalize_aad_version <- function(v) 107 | { 108 | if(v == "v1.0") 109 | v <- 1 110 | else if(v == "v2.0") 111 | v <- 2 112 | if(!(is.numeric(v) && v %in% c(1, 2))) 113 | stop("Invalid AAD version") 114 | v 115 | } 116 | 117 | -------------------------------------------------------------------------------- /R/token.R: -------------------------------------------------------------------------------- 1 | #' Manage Azure Active Directory OAuth 2.0 tokens 2 | #' 3 | #' Use these functions to authenticate with Azure Active Directory (AAD). 4 | #' 5 | #' @param resource For AAD v1.0, the URL of your resource host, or a GUID. For AAD v2.0, a character vector of scopes, each consisting of a URL or GUID along with a path designating the access scope. See 'Details' below. 6 | #' @param tenant Your tenant. This can be a name ("myaadtenant"), a fully qualified domain name ("myaadtenant.onmicrosoft.com" or "mycompanyname.com"), or a GUID. It can also be one of the generic tenants "common", "organizations" or "consumers"; see 'Generic tenants' below. 7 | #' @param app The client/app ID to use to authenticate with. 8 | #' @param password For most authentication flows, this is the password for the _app_ where needed, also known as the client secret. For the resource owner grant, this is your personal account password. See 'Details' below. 9 | #' @param username Your AAD username, if using the resource owner grant. See 'Details' below. 10 | #' @param certificate A file containing the certificate for authenticating with (including the private key), an Azure Key Vault certificate object, or a call to the `cert_assertion` function to build a client assertion with a certificate. See 'Certificate authentication' below. 11 | #' @param auth_type The authentication type. See 'Details' below. 12 | #' @param aad_host URL for your AAD host. For the public Azure cloud, this is `https://login.microsoftonline.com/`. Change this if you are using a government or private cloud. Can also be a full URL, eg `https://mydomain.b2clogin.com/mydomain/other/path/names/oauth2` (this is relevant mainly for Azure B2C logins). 13 | #' @param version The AAD version, either 1 or 2. Authenticating with a personal account as opposed to a work or school account requires AAD 2.0. The default is AAD 1.0 for compatibility reasons, but you should use AAD 2.0 if possible. 14 | #' @param authorize_args An optional list of further parameters for the AAD authorization endpoint. These will be included in the request URI as query parameters. Only used if `auth_type="authorization_code"`. 15 | #' @param token_args An optional list of further parameters for the token endpoint. These will be included in the body of the request for `get_azure_token`, or as URI query parameters for `get_managed_token`. 16 | #' @param use_cache If TRUE and cached credentials exist, use them instead of obtaining a new token. The default value of NULL means to use the cache only if AzureAuth is not running inside a Shiny app. 17 | #' @param on_behalf_of For the on-behalf-of authentication type, a token. This should be either an AzureToken object, or a string containing the JWT-encoded token itself. 18 | #' @param auth_code For the `authorization_code` flow, the code. Only used if `auth_type == "authorization_code"`. 19 | #' @param device_creds For the `device_code` flow, the device credentials used to verify the session between the client and the server. Only used if `auth_type == "device_code"`. 20 | #' 21 | #' @details 22 | #' `get_azure_token` does much the same thing as [httr::oauth2.0_token()], but customised for Azure. It obtains an OAuth token, first by checking if a cached value exists on disk, and if not, acquiring it from the AAD server. `load_azure_token` loads a token given its hash, `delete_azure_token` deletes a cached token given either the credentials or the hash, and `list_azure_tokens` lists currently cached tokens. 23 | #' 24 | #' `get_managed_token` is a specialised function to acquire tokens for a _managed identity_. This is an Azure service, such as a VM or container, that has been assigned its own identity and can be granted access permissions like a regular user. The advantage of managed identities over the other authentication methods (see below) is that you don't have to store a secret password, which improves security. Note that `get_managed_token` can only be used from within the managed identity itself. 25 | #' 26 | #' By default `get_managed_token` retrieves a token using the system-assigned identity for the resource. To obtain a token with a user-assigned identity, pass either the client, object or Azure resource ID in the `token_args` argument. See the examples below. 27 | #' 28 | #' The `resource` arg should be a single URL or GUID for AAD v1.0. For AAD v2.0, it should be a vector of _scopes_, where each scope consists of a URL or GUID along with a path that designates the type of access requested. If a v2.0 scope doesn't have a path, `get_azure_token` will append the `/.default` path with a warning. A special scope is `offline_access`, which requests a refresh token from AAD along with the access token: without this scope, you will have to reauthenticate if you want to refresh the token. 29 | #' 30 | #' The `auth_code` and `device_creds` arguments are intended for use in embedded scenarios, eg when AzureAuth is loaded from within a Shiny web app. They enable the flow authorization step to be separated from the token acquisition step, which is necessary within an app; you can generally ignore these arguments when using AzureAuth interactively or as part of an R script. See the help for [build_authorization_uri] for examples on their use. 31 | #' 32 | #' `token_hash` computes the MD5 hash of its arguments. This is used by AzureAuth to identify tokens for caching purposes. Note that tokens are only cached if you allowed AzureAuth to create a data directory at package startup. 33 | #' 34 | #' One particular use of the `authorize_args` argument is to specify a different redirect URI to the default; see the examples below. 35 | #' 36 | #' @section Authentication methods: 37 | #' 1. Using the **authorization_code** method is a multi-step process. First, `get_azure_token` opens a login window in your browser, where you can enter your AAD credentials. In the background, it loads the [httpuv](https://github.com/rstudio/httpuv) package to listen on a local port. Once you have logged in, the AAD server redirects your browser to a local URL that contains an authorization code. `get_azure_token` retrieves this authorization code and sends it to the AAD access endpoint, which returns the OAuth token. 38 | #' 39 | #' 2. The **device_code** method is similar in concept to authorization_code, but is meant for situations where you are unable to browse the Internet -- for example if you don't have a browser installed or your computer has input constraints. First, `get_azure_token` contacts the AAD devicecode endpoint, which responds with a login URL and an access code. You then visit the URL and enter the code, possibly using a different computer. Meanwhile, `get_azure_token` polls the AAD access endpoint for a token, which is provided once you have entered the code. 40 | #' 41 | #' 3. The **client_credentials** method is much simpler than the above methods, requiring only one step. `get_azure_token` contacts the access endpoint, passing it either the app secret or the certificate assertion (which you supply in the `password` or `certificate` argument respectively). Once the credentials are verified, the endpoint returns the token. This is the method typically used by service accounts. 42 | #' 43 | #' 4. The **resource_owner** method also requires only one step. In this method, `get_azure_token` passes your (personal) username and password to the AAD access endpoint, which validates your credentials and returns the token. 44 | #' 45 | #' 5. The **on_behalf_of** method is used to authenticate with an Azure resource by passing a token obtained beforehand. It is mostly used by intermediate apps to authenticate for users. In particular, you can use this method to obtain tokens for multiple resources, while only requiring the user to authenticate once: see the examples below. 46 | #' 47 | #' If the authentication method is not specified, it is chosen based on the presence or absence of the other arguments, and whether httpuv is installed. 48 | #' 49 | #' The httpuv package must be installed to use the authorization_code method, as this requires a web server to listen on the (local) redirect URI. See [httr::oauth2.0_token] for more information; note that Azure does not support the `use_oob` feature of the httr OAuth 2.0 token class. 50 | #' 51 | #' Similarly, since the authorization_code method opens a browser to load the AAD authorization page, your machine must have an Internet browser installed that can be run from inside R. In particular, if you are using a Linux [Data Science Virtual Machine](https://azure.microsoft.com/en-us/services/virtual-machines/data-science-virtual-machines/) in Azure, you may run into difficulties; use one of the other methods instead. 52 | #' 53 | #' @section Certificate authentication: 54 | #' OAuth tokens can be authenticated via an SSL/TLS certificate, which is considered more secure than a client secret. To do this, use the `certificate` argument, which can contain any of the following: 55 | #' - The name of a PEM or PFX file, containing _both_ the private key and the public certificate. 56 | #' - A certificate object from the AzureKeyVault package, representing a cert stored in the Key Vault service. 57 | #' - A call to the `cert_assertion()` function to customise details of the requested token, eg the duration, expiry date, custom claims, etc. See the examples below. 58 | #' 59 | #' @section Generic tenants: 60 | #' 61 | #' There are 3 generic values that can be used as tenants when authenticating: 62 | #' 63 | #' | Tenant | Description | 64 | #' | ------ | ----------- | 65 | #' | `common` | Allows users with both personal Microsoft accounts and work/school accounts from Azure AD to sign into the application. | 66 | #' | `organizations` | Allows only users with work/school accounts from Azure AD to sign into the application. | 67 | #' | `consumers` | Allows only users with personal Microsoft accounts (MSA) to sign into the application. | 68 | #' 69 | #' @section Authentication vs authorization: 70 | #' Azure Active Directory can be used for two purposes: _authentication_ (verifying that a user is who they claim they are) and _authorization_ (granting a user permission to access a resource). In AAD, a successful authorization process concludes with the granting of an OAuth 2.0 access token, as discussed above. Authentication uses the same process but concludes by granting an ID token, as defined in the OpenID Connect protocol. 71 | #' 72 | #' `get_azure_token` can be used to obtain ID tokens along with regular OAuth access tokens, when using an interactive flow (authorization_code or device_code). The behaviour depends on the AAD version: 73 | #' 74 | #' When retrieving ID tokens, the behaviour depends on the AAD version: 75 | #' - AAD v1.0 will return an ID token as well as the access token by default; you don't have to do anything extra. However, AAD v1.0 will not _refresh_ the ID token when it expires; you must reauthenticate to get a new one. To ensure you don't pull the cached version of the credentials, specify `use_cache=FALSE` in the calls to `get_azure_token`. 76 | #' - Unlike AAD v1.0, AAD v2.0 does not return an ID token by default. To get a token, include `openid` as a scope. On the other hand it _does_ refresh the ID token, so bypassing the cache is not needed. It's recommended to use AAD v2.0 if you only want an ID token. 77 | #' 78 | #' If you _only_ want to do authentication and not authorization (for example if your app does not use any Azure resources), specify the `resource` argument as follows: 79 | #' - For AAD v1.0, use a blank resource (`resource=""`). 80 | #' - For AAD v2.0, use `resource="openid"` without any other elements. Optionally you can add `"offline_access"` as a 2nd element if you want a refresh token as well. 81 | #' 82 | #' See also the examples below. 83 | #' 84 | #' @section Caching: 85 | #' AzureAuth caches tokens based on all the inputs to `get_azure_token` as listed above. Tokens are cached in a custom, user-specific directory, created with the rappdirs package. On recent Windows versions, this will usually be in the location `C:\\Users\\(username)\\AppData\\Local\\AzureR`. On Linux, it will be in `~/.config/AzureR`, and on MacOS, it will be in `~/Library/Application Support/AzureR`. Alternatively, you can specify the location of the directory in the environment variable `R_AZURE_DATA_DIR`. Note that a single directory is used for all tokens, and the working directory is not touched (which significantly lessens the risk of accidentally introducing cached tokens into source control). 86 | #' 87 | #' To list all cached tokens on disk, use `list_azure_tokens`. This returns a list of token objects, named according to their MD5 hashes. 88 | #' 89 | #' To delete a cached token, use `delete_azure_token`. This takes the same inputs as `get_azure_token`, or you can specify the MD5 hash directly in the `hash` argument. 90 | #' 91 | #' To delete all files in the caching directory, use `clean_token_directory`. 92 | #' 93 | #' @section Refreshing: 94 | #' A token object can be refreshed by calling its `refresh()` method. If the token's credentials contain a refresh token, this is used; otherwise a new access token is obtained by reauthenticating. 95 | #' 96 | #' Note that in AAD, a refresh token can be used to obtain an access token for any resource or scope that you have permissions for. Thus, for example, you could use a refresh token issued on a request for Azure Resource Manager (`https://management.azure.com/`) to obtain a new access token for Microsoft Graph (`https://graph.microsoft.com/`). 97 | #' 98 | #' To obtain an access token for a new resource, change the object's `resource` (for an AAD v1.0 token) or `scope` field (for an AAD v2.0 token) before calling `refresh()`. If you _also_ want to retain the token for the old resource, you should call the `clone()` method first to create a copy. See the examples below. 99 | #' 100 | #' @section Value: 101 | #' For `get_azure_token`, an object inheriting from `AzureToken`. The specific class depends on the authentication flow: `AzureTokenAuthCode`, `AzureTokenDeviceCode`, `AzureTokenClientCreds`, `AzureTokenOnBehalfOf`, `AzureTokenResOwner`. For `get_managed_token`, a similar object of class `AzureTokenManaged`. 102 | #' 103 | #' For `list_azure_tokens`, a list of such objects retrieved from disk. 104 | #' 105 | #' The actual credentials that are returned from the authorization endpoint can be found in the `credentials` field, the same as with a `httr::Token` object. The access token (if present) will be `credentials$access_token`, and the ID token (if present) will be `credentials$id_token`. Use these if you are manually constructing a HTTP request and need to insert an "Authorization" header, for example. 106 | #' 107 | #' @seealso 108 | #' [AzureToken], [httr::oauth2.0_token], [httr::Token], [cert_assertion], 109 | #' [build_authorization_uri], [get_device_creds] 110 | #' 111 | #' [Azure Active Directory for developers](https://docs.microsoft.com/en-us/azure/active-directory/develop/), 112 | #' [Managed identities overview](https://docs.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/overview) 113 | #' [Device code flow on OAuth.com](https://www.oauth.com/oauth2-servers/device-flow/token-request/), 114 | #' [OAuth 2.0 RFC](https://tools.ietf.org/html/rfc6749) for the gory details on how OAuth works 115 | #' 116 | #' @examples 117 | #' \dontrun{ 118 | #' 119 | #' # authenticate with Azure Resource Manager: 120 | #' # no user credentials are supplied, so this will use the authorization_code 121 | #' # method if httpuv is installed, and device_code if not 122 | #' get_azure_token("https://management.azure.com/", tenant="mytenant", app="app_id") 123 | #' 124 | #' # you can force a specific authentication method with the auth_type argument 125 | #' get_azure_token("https://management.azure.com/", tenant="mytenant", app="app_id", 126 | #' auth_type="device_code") 127 | #' 128 | #' # to default to the client_credentials method, supply the app secret as the password 129 | #' get_azure_token("https://management.azure.com/", tenant="mytenant", app="app_id", 130 | #' password="app_secret") 131 | #' 132 | #' # authenticate to your resource with the resource_owner method: provide your username and password 133 | #' get_azure_token("https://myresource/", tenant="mytenant", app="app_id", 134 | #' username="user", password="abcdefg") 135 | #' 136 | #' # obtaining multiple tokens: authenticate (interactively) once... 137 | #' tok0 <- get_azure_token("serviceapp_id", tenant="mytenant", app="clientapp_id", 138 | #' auth_type="authorization_code") 139 | #' # ...then get tokens for each resource (Resource Manager and MS Graph) with on_behalf_of 140 | #' tok1 <- get_azure_token("https://management.azure.com/", tenant="mytenant", app="serviceapp_id", 141 | #' password="serviceapp_secret", on_behalf_of=tok0) 142 | #' tok2 <- get_azure_token("https://graph.microsoft.com/", tenant="mytenant", app="serviceapp_id", 143 | #' password="serviceapp_secret", on_behalf_of=tok0) 144 | #' 145 | #' 146 | #' # authorization_code flow with app registered in AAD as a web rather than a native client: 147 | #' # supply the client secret in the password arg 148 | #' get_azure_token("https://management.azure.com/", "mytenant", "app_id", 149 | #' password="app_secret", auth_type="authorization_code") 150 | #' 151 | #' 152 | #' # use a different redirect URI to the default localhost:1410 153 | #' get_azure_token("https://management.azure.com/", tenant="mytenant", app="app_id", 154 | #' authorize_args=list(redirect_uri="http://localhost:8000")) 155 | #' 156 | #' 157 | #' # request an AAD v1.0 token for Resource Manager (the default) 158 | #' token1 <- get_azure_token("https://management.azure.com/", "mytenant", "app_id") 159 | #' 160 | #' # same request to AAD v2.0, along with a refresh token 161 | #' token2 <- get_azure_token(c("https://management.azure.com/.default", "offline_access"), 162 | #' "mytenant", "app_id", version=2) 163 | #' 164 | #' # requesting multiple scopes (Microsoft Graph) with AAD 2.0 165 | #' get_azure_token(c("https://graph.microsoft.com/User.Read.All", 166 | #' "https://graph.microsoft.com/User.ReadWrite.All", 167 | #' "https://graph.microsoft.com/Directory.ReadWrite.All", 168 | #' "offline_access"), 169 | #' "mytenant", "app_id", version=2) 170 | #' 171 | #' 172 | #' # list saved tokens 173 | #' list_azure_tokens() 174 | #' 175 | #' # delete a saved token from disk 176 | #' delete_azure_token(resource="https://myresource/", tenant="mytenant", app="app_id", 177 | #' username="user", password="abcdefg") 178 | #' 179 | #' # delete a saved token by specifying its MD5 hash 180 | #' delete_azure_token(hash="7ea491716e5b10a77a673106f3f53bfd") 181 | #' 182 | #' 183 | #' # authenticating for B2C logins (custom AAD host) 184 | #' get_azure_token("https://mydomain.com", "mytenant", "app_id", "password", 185 | #' aad_host="https://mytenant.b2clogin.com/tfp/mytenant.onmicrosoft.com/custom/oauth2") 186 | #' 187 | #' 188 | #' # authenticating with a certificate 189 | #' get_azure_token("https://management.azure.com/", "mytenant", "app_id", 190 | #' certificate="mycert.pem") 191 | #' 192 | #' # authenticating with a certificate stored in Azure Key Vault 193 | #' cert <- AzureKeyVault::key_vault("myvault")$certificates$get("mycert") 194 | #' get_azure_token("https://management.azure.com/", "mytenant", "app_id", 195 | #' certificate=cert) 196 | #' 197 | #' # get a token valid for 2 hours (default is 1 hour) 198 | #' get_azure_token("https://management.azure.com/", "mytenant", "app_id", 199 | #' certificate=cert_assertion("mycert.pem", duration=2*3600)) 200 | #' 201 | #' 202 | #' # ID token with AAD v1.0 203 | #' # if you only want an ID token, set the resource to blank ("") 204 | #' tok <- get_azure_token("", "mytenant", "app_id", use_cache=FALSE) 205 | #' extract_jwt(tok, "id") 206 | #' 207 | #' # ID token with AAD v2.0 (recommended) 208 | #' tok2 <- get_azure_token(c("openid", "offline_access"), "mytenant", "app_id", version=2) 209 | #' extract_jwt(tok2, "id") 210 | #' 211 | #' 212 | #' # get a token from within a managed identity (VM, container or service) 213 | #' get_managed_token("https://management.azure.com/") 214 | #' 215 | #' # get a token from a managed identity, with a user-defined identity: 216 | #' # specify one of the identity's object_id, client_id and mi_res_id (Azure resource ID) 217 | #' # you can get these values via the Azure Portal or Azure CLI 218 | #' get_managed_token("https://management.azure.com/", token_args=list( 219 | #' mi_res_id="/subscriptions/zzzz-zzzz/resourceGroups/resgroupname/..." 220 | #' )) 221 | #' 222 | #' # use a refresh token from one resource to get an access token for another resource 223 | #' tok <- get_azure_token("https://myresource", "mytenant", "app_id") 224 | #' tok2 <- tok$clone() 225 | #' tok2$resource <- "https://anotherresource" 226 | #' tok2$refresh() 227 | #' 228 | #' # same for AAD v2.0 229 | #' tok <- get_azure_token(c("https://myresource/.default", "offline_access"), 230 | #' "mytenant", "app_id", version=2) 231 | #' tok2 <- tok$clone() 232 | #' tok2$scope <- c("https://anotherresource/.default", "offline_access") 233 | #' tok2$refresh() 234 | #' 235 | #' 236 | #' # manually adding auth header for a HTTP request 237 | #' tok <- get_azure_token("https://myresource", "mytenant", "app_id") 238 | #' header <- httr::add_headers(Authorization=paste("Bearer", tok$credentials$access_token)) 239 | #' httr::GET("https://myresource/path/for/call", header, ...) 240 | #' 241 | #' } 242 | #' @export 243 | get_azure_token <- function(resource, tenant, app, password=NULL, username=NULL, certificate=NULL, auth_type=NULL, 244 | aad_host="https://login.microsoftonline.com/", version=1, 245 | authorize_args=list(), token_args=list(), 246 | use_cache=NULL, on_behalf_of=NULL, auth_code=NULL, device_creds=NULL) 247 | { 248 | auth_type <- select_auth_type(password, username, certificate, auth_type, on_behalf_of) 249 | 250 | common_args <- list( 251 | resource=resource, 252 | tenant=tenant, 253 | app=app, 254 | password=password, 255 | username=username, 256 | certificate=certificate, 257 | aad_host=aad_host, 258 | version=version, 259 | token_args=token_args, 260 | use_cache=use_cache 261 | ) 262 | 263 | switch(auth_type, 264 | authorization_code= 265 | AzureTokenAuthCode$new(common_args, authorize_args, auth_code), 266 | device_code= 267 | AzureTokenDeviceCode$new(common_args, device_creds), 268 | client_credentials= 269 | AzureTokenClientCreds$new(common_args), 270 | on_behalf_of= 271 | AzureTokenOnBehalfOf$new(common_args, on_behalf_of), 272 | resource_owner= 273 | AzureTokenResOwner$new(common_args), 274 | stop("Unknown authentication method ", auth_type, call.=FALSE)) 275 | } 276 | 277 | 278 | #' @param hash The MD5 hash of this token, computed from the above inputs. Used by `load_azure_token` and `delete_azure_token` to identify a cached token to load and delete, respectively. 279 | #' @param confirm For `delete_azure_token`, whether to prompt for confirmation before deleting a token. 280 | #' @rdname get_azure_token 281 | #' @export 282 | delete_azure_token <- function(resource, tenant, app, password=NULL, username=NULL, certificate=NULL, auth_type=NULL, 283 | aad_host="https://login.microsoftonline.com/", version=1, 284 | authorize_args=list(), token_args=list(), on_behalf_of=NULL, 285 | hash=NULL, confirm=TRUE) 286 | { 287 | if(!dir.exists(AzureR_dir())) 288 | return(invisible(NULL)) 289 | 290 | if(is.null(hash)) 291 | hash <- token_hash(resource, tenant, app, password, username, certificate, auth_type, aad_host, version, 292 | authorize_args, token_args, on_behalf_of) 293 | 294 | if(confirm && interactive() && 295 | !get_confirmation("Do you really want to delete this Azure Active Directory token?", FALSE)) 296 | return(invisible(NULL)) 297 | 298 | file.remove(file.path(AzureR_dir(), hash)) 299 | invisible(NULL) 300 | } 301 | 302 | 303 | #' @rdname get_azure_token 304 | #' @export 305 | load_azure_token <- function(hash) 306 | { 307 | readRDS(file.path(AzureR_dir(), hash)) 308 | } 309 | 310 | 311 | #' @rdname get_azure_token 312 | #' @export 313 | clean_token_directory <- function(confirm=TRUE) 314 | { 315 | if(!dir.exists(AzureR_dir())) 316 | return(invisible(NULL)) 317 | 318 | if(confirm && interactive() && 319 | !get_confirmation("Do you really want to remove all files in the AzureR token directory?", FALSE)) 320 | return(invisible(NULL)) 321 | 322 | toks <- dir(AzureR_dir(), full.names=TRUE) 323 | file.remove(toks) 324 | invisible(NULL) 325 | } 326 | 327 | 328 | #' @rdname get_azure_token 329 | #' @export 330 | list_azure_tokens <- function() 331 | { 332 | tokens <- dir(AzureR_dir(), pattern="[0-9a-f]{32}", full.names=TRUE) 333 | lst <- lapply(tokens, function(fname) 334 | { 335 | x <- try(readRDS(fname), silent=TRUE) 336 | if(is_azure_token(x)) 337 | x 338 | else NULL 339 | }) 340 | names(lst) <- basename(tokens) 341 | lst[!sapply(lst, is.null)] 342 | } 343 | 344 | 345 | #' @rdname get_azure_token 346 | #' @export 347 | token_hash <- function(resource, tenant, app, password=NULL, username=NULL, certificate=NULL, auth_type=NULL, 348 | aad_host="https://login.microsoftonline.com/", version=1, 349 | authorize_args=list(), token_args=list(), on_behalf_of=NULL) 350 | { 351 | # create dummy object 352 | object <- get_azure_token( 353 | resource=resource, 354 | tenant=tenant, 355 | app=app, 356 | password=password, 357 | username=username, 358 | certificate=certificate, 359 | auth_type=auth_type, 360 | aad_host=aad_host, 361 | version=version, 362 | authorize_args=authorize_args, 363 | token_args=token_args, 364 | on_behalf_of=on_behalf_of, 365 | use_cache=NA 366 | ) 367 | object$hash() 368 | } 369 | 370 | 371 | token_hash_internal <- function(...) 372 | { 373 | msg <- serialize(list(...), NULL, version=2) 374 | paste(openssl::md5(msg[-(1:14)]), collapse="") 375 | } 376 | 377 | 378 | # handle different behaviour of file_path on Windows/Linux wrt trailing / 379 | construct_path <- function(...) 380 | { 381 | sub("/$", "", file.path(..., fsep="/")) 382 | } 383 | 384 | 385 | is_empty <- function(x) 386 | { 387 | is.null(x) || length(x) == 0 388 | } 389 | 390 | 391 | #' @param object For `is_azure_token`, `is_azure_v1_token` and `is_azure_v2_token`, an R object. 392 | #' @rdname get_azure_token 393 | #' @export 394 | is_azure_token <- function(object) 395 | { 396 | R6::is.R6(object) && inherits(object, "AzureToken") 397 | } 398 | 399 | 400 | #' @rdname get_azure_token 401 | #' @export 402 | is_azure_v1_token <- function(object) 403 | { 404 | is_azure_token(object) && object$version == 1 405 | } 406 | 407 | 408 | #' @rdname get_azure_token 409 | #' @export 410 | is_azure_v2_token <- function(object) 411 | { 412 | is_azure_token(object) && object$version == 2 413 | } 414 | -------------------------------------------------------------------------------- /R/utils.R: -------------------------------------------------------------------------------- 1 | select_auth_type <- function(password, username, certificate, auth_type, on_behalf_of) 2 | { 3 | if(!is.null(auth_type)) 4 | { 5 | if(!auth_type %in% 6 | c("authorization_code", "device_code", "client_credentials", "resource_owner", "on_behalf_of", 7 | "managed")) 8 | stop("Invalid authentication method") 9 | return(auth_type) 10 | } 11 | 12 | got_pwd <- !is.null(password) 13 | got_user <- !is.null(username) 14 | got_cert <- !is.null(certificate) 15 | got_httpuv <- system.file(package="httpuv") != "" 16 | 17 | auth_type <- if(got_pwd && got_user && !got_cert) 18 | "resource_owner" 19 | else if(!got_pwd && !got_user && !got_cert) 20 | { 21 | if(!got_httpuv) 22 | { 23 | message("httpuv not installed, defaulting to device code authentication") 24 | "device_code" 25 | } 26 | else "authorization_code" 27 | } 28 | else if(!got_pwd && !got_cert && got_user && got_httpuv) 29 | "authorization_code" 30 | else if((got_pwd && !got_user) || got_cert) 31 | { 32 | if(is_empty(on_behalf_of)) 33 | "client_credentials" 34 | else "on_behalf_of" 35 | } 36 | else stop("Can't select authentication method", call.=FALSE) 37 | 38 | message("Using ", auth_type, " flow") 39 | auth_type 40 | } 41 | 42 | 43 | process_aad_response <- function(res) 44 | { 45 | status <- httr::status_code(res) 46 | if(status >= 300) 47 | { 48 | cont <- httr::content(res) 49 | 50 | msg <- if(is.character(cont)) 51 | cont 52 | else if(is.list(cont) && is.character(cont$error_description)) 53 | cont$error_description 54 | else "" 55 | 56 | msg <- paste0("obtain Azure Active Directory token. Message:\n", sub("\\.$", "", msg)) 57 | list(token=httr::stop_for_status(status, msg)) 58 | } 59 | else httr::content(res) 60 | } 61 | 62 | 63 | # need to capture bad scopes before requesting auth code 64 | # v2.0 endpoint will show error page rather than redirecting, causing get_azure_token to wait forever 65 | verify_v2_scope <- function(scope) 66 | { 67 | # some OpenID scopes get a pass 68 | openid_scopes <- c("openid", "email", "profile", "offline_access") 69 | if(scope %in% openid_scopes) 70 | return(scope) 71 | 72 | # but not all 73 | bad_scopes <- c("address", "phone") 74 | if(scope %in% bad_scopes) 75 | stop("Unsupported OpenID scope: ", scope, call.=FALSE) 76 | 77 | # is it a URI or GUID? 78 | valid_uri <- !is.null(httr::parse_url(scope)$scheme) 79 | valid_guid <- is_guid(sub("/.*$", "", scope)) 80 | if(!valid_uri && !valid_guid) 81 | stop("Invalid scope (must be a URI or GUID): ", scope, call.=FALSE) 82 | 83 | # if a URI or GUID, check that there is a valid scope in the path 84 | if(valid_uri) 85 | { 86 | uri <- httr::parse_url(scope) 87 | if(uri$path == "") 88 | { 89 | warning("No path supplied for scope ", scope, "; setting to /.default", call.=FALSE) 90 | uri$path <- ".default" 91 | scope <- httr::build_url(uri) 92 | } 93 | } 94 | else 95 | { 96 | path <- sub("^[^/]+/?", "", scope) 97 | if(path == "") 98 | { 99 | warning("No path supplied for scope ", scope, "; setting to /.default", call.=FALSE) 100 | scope <- sub("//", "/", paste0(scope, "/.default")) 101 | } 102 | } 103 | scope 104 | } 105 | 106 | 107 | aad_uri <- function(aad_host, tenant, version, type, query=list()) 108 | { 109 | uri <- httr::parse_url(aad_host) 110 | uri$query <- query 111 | 112 | uri$path <- if(nchar(uri$path) == 0) 113 | { 114 | if(version == 1) 115 | file.path(tenant, "oauth2", type) 116 | else file.path(tenant, "oauth2/v2.0", type) 117 | } 118 | else file.path(uri$path, type) 119 | 120 | httr::build_url(uri) 121 | } 122 | 123 | 124 | paste_v2_scopes <- function(scope) 125 | { 126 | paste(scope, collapse=" ") 127 | } 128 | 129 | 130 | # display confirmation prompt, return TRUE/FALSE (no NA) 131 | get_confirmation <- function(msg, default=TRUE) 132 | { 133 | ok <- if(getRversion() < numeric_version("3.5.0")) 134 | { 135 | msg <- paste(msg, if(default) "(Yes/no/cancel) " else "(yes/No/cancel) ") 136 | yn <- readline(msg) 137 | if(nchar(yn) == 0) 138 | default 139 | else tolower(substr(yn, 1, 1)) == "y" 140 | } 141 | else utils::askYesNo(msg, default) 142 | isTRUE(ok) 143 | } 144 | 145 | 146 | in_shiny <- function() 147 | { 148 | ("shiny" %in% loadedNamespaces()) && shiny::isRunning() 149 | } 150 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AzureAuth 2 | 3 | [![CRAN](https://www.r-pkg.org/badges/version/AzureAuth)](https://cran.r-project.org/package=AzureAuth) 4 | ![Downloads](https://cranlogs.r-pkg.org/badges/AzureAuth) 5 | ![R-CMD-check](https://github.com/Azure/AzureAuth/workflows/R-CMD-check/badge.svg) 6 | 7 | AzureAuth provides [Azure Active Directory](https://docs.microsoft.com/azure/active-directory/develop/) (AAD) authentication functionality for R users of Microsoft's Azure cloud. Use this package to obtain OAuth 2.0 tokens for Azure services including Azure Resource Manager, Azure Storage and others. Both AAD v1.0 and v2.0 are supported. 8 | 9 | The primary repo for this package is at https://github.com/Azure/AzureAuth; please submit issues and PRs there. It is also mirrored at the Cloudyr org at https://github.com/cloudyr/AzureAuth. You can install the development version of the package with `devtools::install_github("Azure/AzureAuth")`. 10 | 11 | ## Obtaining tokens 12 | 13 | The main function in AzureAuth is `get_azure_token`, which obtains an OAuth token from AAD. The token is cached in a user-specific directory using the [rappdirs](https://github.com/r-lib/rappdirs) package, and future requests will use the cached token without needing you to reauthenticate. 14 | 15 | ```r 16 | library(AzureAuth) 17 | 18 | token <- get_azure_token(resource="myresource", tenant="mytenant", app="app_id", ...) 19 | ``` 20 | 21 | For reasons of CRAN policy, the first time AzureAuth is loaded, it will prompt you for permission to create this directory. Unless you have a specific reason otherwise, it's recommended that you allow the directory to be created. Note that most other cloud engineering tools save credentials in this way, including Docker, Kubernetes, and the Azure CLI itself. The prompt only appears in an interactive session; if AzureAuth is loaded in a batch script, the directory is not created if it doesn't already exist. 22 | 23 | Other supplied functions include `list_azure_tokens`, `delete_azure_token` and `clean_token_directory`, to let you manage the token cache. 24 | 25 | AzureAuth supports the following methods for authenticating with AAD: **authorization_code**, **device_code**, **client_credentials**, **resource_owner** and **on_behalf_of**. 26 | 27 | 1. Using the **authorization_code** method is a multi-step process. First, `get_azure_token` opens a login window in your browser, where you can enter your AAD credentials. In the background, it loads the [httpuv](https://github.com/rstudio/httpuv) package to listen on a local port. Once you have logged in, the AAD server redirects your browser to a local URL that contains an authorization code. `get_azure_token` retrieves this authorization code and sends it to the AAD access endpoint, which returns the OAuth token. 28 | 29 | ```r 30 | # obtain a token using authorization_code 31 | # no user credentials needed 32 | get_azure_token("myresource", "mytenant", "app_id", auth_type="authorization_code") 33 | ``` 34 | 35 | 2. The **device_code** method is similar in concept to authorization_code, but is meant for situations where you are unable to browse the Internet -- for example if you don't have a browser installed or your computer has input constraints. First, `get_azure_token` contacts the AAD devicecode endpoint, which responds with a login URL and an access code. You then visit the URL and enter the code, possibly using a different computer. Meanwhile, `get_azure_token` polls the AAD access endpoint for a token, which is provided once you have entered the code. 36 | 37 | ```r 38 | # obtain a token using device_code 39 | # no user credentials needed 40 | get_azure_token("myresource", "mytenant", "app_id", auth_type="device_code") 41 | ``` 42 | 43 | 3. The **client_credentials** method is much simpler than the above methods, requiring only one step. `get_azure_token` contacts the access endpoint, passing it the credentials. This can be either a client secret or a certificate, which you supply in the `password` or `certificate` argument respectively. Once the credentials are verified, the endpoint returns the token. 44 | 45 | ```r 46 | # obtain a token using client_credentials 47 | # supply credentials in password arg 48 | get_azure_token("myresource", "mytenant", "app_id", 49 | password="client_secret", auth_type="client_credentials") 50 | 51 | # can also supply a client certificate as a PEM/PFX file... 52 | get_azure_token("myresource", "mytenant", "app_id", 53 | certificate="mycert.pem", auth_type="client_credentials") 54 | 55 | # ... or as an object in Azure Key Vault 56 | cert <- AzureKeyVault::key_vault("myvault")$certificates$get("mycert") 57 | get_azure_token("myresource", "mytenant", "app_id", 58 | certificate=cert, auth_type="client_credentials") 59 | ``` 60 | 61 | 4. The **resource_owner** method also requires only one step. In this method, `get_azure_token` passes your (personal) username and password to the AAD access endpoint, which validates your credentials and returns the token. 62 | 63 | ```r 64 | # obtain a token using resource_owner 65 | # supply credentials in username and password args 66 | get_azure_token("myresource", "mytenant", "app_id", 67 | username="myusername", password="mypassword", auth_type="resource_owner") 68 | ``` 69 | 70 | 5. The **on_behalf_of** method is used to authenticate with an Azure resource by passing a token obtained beforehand. It is mostly used by intermediate apps to authenticate for users. In particular, you can use this method to obtain tokens for multiple resources, while only requiring the user to authenticate once. 71 | 72 | ```r 73 | # obtaining multiple tokens: authenticate (interactively) once... 74 | tok0 <- get_azure_token("serviceapp_id", "mytenant", "clientapp_id", auth_type="authorization_code") 75 | # ...then get tokens for each resource with on_behalf_of 76 | tok1 <- get_azure_token("resource1", "mytenant," "serviceapp_id", 77 | password="serviceapp_secret", auth_type="on_behalf_of", on_behalf_of=tok0) 78 | tok2 <- get_azure_token("resource2", "mytenant," "serviceapp_id", 79 | password="serviceapp_secret", auth_type="on_behalf_of", on_behalf_of=tok0) 80 | ``` 81 | 82 | If you don't specify the method, `get_azure_token` makes a best guess based on the presence or absence of the other authentication arguments, and whether httpuv is installed. 83 | 84 | ### Managed identities 85 | 86 | AzureAuth provides `get_managed_token` to obtain tokens from within a managed identity. This is a VM, service or container in Azure that can authenticate as itself, which removes the need to save secret passwords or certificates. 87 | 88 | ```r 89 | # run this from within an Azure VM or container for which an identity has been setup 90 | get_managed_token("myresource") 91 | ``` 92 | 93 | ### Inside a web app 94 | 95 | Using the interactive flows (authorization_code and device_code) from within a Shiny app requires separating the authorization (logging in to Azure) step from the token acquisition step. For this purpose, AzureAuth provides the `build_authorization_uri` and `get_device_creds` functions. You can use these from within your app to carry out the authorization, and then pass the resulting credentials to `get_azure_token` itself. See the "Authenticating from Shiny" vignette for an example app. 96 | 97 | ## OpenID Connect 98 | 99 | You can also use `get_azure_token` to obtain ID tokens, in addition to access tokens. 100 | 101 | With AAD v1.0, using an interactive authentication flow (authorization_code or device_code) will return an ID token by default -- you don't have to do anything extra. However, AAD v1.0 will _not_ refresh the ID token when it expires (only the access token). Because of this, specify `use_cache=FALSE` to avoid picking up cached token credentials which may have been refreshed previously. 102 | 103 | AAD v2.0 does not return an ID token by default, but you can get one by adding the `openid` scope. Again, this applies only to interactive authentication. If you only want an ID token, it's recommended to use AAD v2.0. 104 | 105 | ```r 106 | # ID token with AAD v1.0 107 | tok <- get_azure_token("", "mytenant", "app_id", use_cache=FALSE) 108 | extract_jwt(tok, "id") 109 | 110 | # ID token with AAD v2.0 (recommended) 111 | tok2 <- get_azure_token(c("openid", "offline_access"), "mytenant", "app_id", version=2) 112 | extract_jwt(tok2, "id") 113 | ``` 114 | 115 | ## Acknowledgements 116 | 117 | The AzureAuth interface is based on the OAuth framework in the [httr](https://github.com/r-lib/httr) package, customised and streamlined for Azure. It is an independent implementation of OAuth, but benefited greatly from the work done by Hadley Wickham and the rest of the httr development team. 118 | 119 | ---- 120 |

121 | 122 | -------------------------------------------------------------------------------- /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 definition of a security vulnerability](https://aka.ms/opensource/security/definition), 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://aka.ms/opensource/security/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 [Microsoft Security Response Center PGP Key page](https://aka.ms/opensource/security/pgpkey). 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://aka.ms/opensource/security/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://aka.ms/opensource/security/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://aka.ms/opensource/security/cvd). 40 | 41 | 42 | -------------------------------------------------------------------------------- /man/AzureR_dir.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/AzureAuth.R 3 | \name{AzureR_dir} 4 | \alias{AzureR_dir} 5 | \alias{create_AzureR_dir} 6 | \title{Data directory for AzureR packages} 7 | \usage{ 8 | AzureR_dir() 9 | 10 | create_AzureR_dir() 11 | } 12 | \value{ 13 | A string containing the data directory. 14 | } 15 | \description{ 16 | Data directory for AzureR packages 17 | } 18 | \details{ 19 | AzureAuth can save your authentication credentials in a user-specific directory, using the rappdirs package. On recent Windows versions, this will usually be in the location \verb{C:\\\\Users\\\\(username)\\\\AppData\\\\Local\\\\AzureR}. On Unix/Linux, it will be in \verb{~/.local/share/AzureR}, and on MacOS, it will be in \verb{~/Library/Application Support/AzureR}.Alternatively, you can specify the location of the directory in the environment variable \code{R_AZURE_DATA_DIR}. AzureAuth does not modify R's working directory, which significantly lessens the risk of accidentally introducing cached tokens into source control. 20 | 21 | On package startup, if this directory does not exist, AzureAuth will prompt you for permission to create it. It's recommended that you allow the directory to be created, as otherwise you will have to reauthenticate with Azure every time. Note that many cloud engineering tools, including the \href{https://docs.microsoft.com/en-us/cli/azure/?view=azure-cli-latest}{Azure CLI}, save authentication credentials in this way. The prompt only appears in an interactive session (in the sense that \code{interactive()} returns TRUE); if AzureAuth is loaded in a batch script, the directory is not created if it doesn't already exist. 22 | 23 | \code{create_AzureR_dir} is a utility function to create the caching directory manually. This can be useful not just for non-interactive sessions, but also Jupyter and R notebooks, which are not \emph{technically} interactive in that \code{interactive()} returns FALSE. 24 | 25 | The caching directory is also used by other AzureR packages, notably AzureRMR (for storing Resource Manager logins) and AzureGraph (for Microsoft Graph logins). You should not save your own files in it; instead, treat it as something internal to the AzureR packages. 26 | } 27 | \seealso{ 28 | \link{get_azure_token} 29 | 30 | \link[rappdirs:user_data_dir]{rappdirs::user_data_dir} 31 | } 32 | -------------------------------------------------------------------------------- /man/AzureToken.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/AzureToken.R, R/classes.R 3 | \docType{class} 4 | \name{AzureToken} 5 | \alias{AzureToken} 6 | \alias{AzureTokenAuthCode} 7 | \alias{AzureTokenDeviceCode} 8 | \alias{AzureTokenClientCreds} 9 | \alias{AzureTokenOnBehalfOf} 10 | \alias{AzureTokenResOwner} 11 | \alias{AzureTokenManaged} 12 | \title{Azure OAuth authentication} 13 | \format{ 14 | An R6 object representing an Azure Active Directory token and its associated credentials. \code{AzureToken} is the base class, and the others inherit from it. 15 | } 16 | \description{ 17 | Azure OAuth 2.0 token classes, with an interface based on the \link[httr:Token-class]{Token2.0 class} in httr. Rather than calling the initialization methods directly, tokens should be created via \code{\link[=get_azure_token]{get_azure_token()}}. 18 | } 19 | \section{Methods}{ 20 | 21 | \itemize{ 22 | \item \code{refresh}: Refreshes the token. For expired tokens without an associated refresh token, refreshing really means requesting a new token. 23 | \item \code{validate}: Checks if the token has not yet expired. Note that a token may be invalid for reasons other than having expired, eg if it is revoked on the server. 24 | \item \code{hash}: Computes an MD5 hash on the input fields of the object. Used internally for identification purposes when caching. 25 | \item \code{cache}: Stores the token on disk for use in future sessions. 26 | } 27 | } 28 | 29 | \seealso{ 30 | \link{get_azure_token}, \link[httr:Token-class]{httr::Token} 31 | } 32 | -------------------------------------------------------------------------------- /man/authorization.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/flow_init.R 3 | \name{build_authorization_uri} 4 | \alias{build_authorization_uri} 5 | \alias{get_device_creds} 6 | \title{Standalone OAuth authorization functions} 7 | \usage{ 8 | build_authorization_uri( 9 | resource, 10 | tenant, 11 | app, 12 | username = NULL, 13 | ..., 14 | aad_host = "https://login.microsoftonline.com/", 15 | version = 1 16 | ) 17 | 18 | get_device_creds( 19 | resource, 20 | tenant, 21 | app, 22 | aad_host = "https://login.microsoftonline.com/", 23 | version = 1 24 | ) 25 | } 26 | \arguments{ 27 | \item{resource, tenant, app, aad_host, version}{See the corresponding arguments for \link{get_azure_token}.} 28 | 29 | \item{username}{For \code{build_authorization_uri}, an optional login hint to be sent to the authorization endpoint.} 30 | 31 | \item{...}{Named arguments that will be added to the authorization URI as query parameters.} 32 | } 33 | \value{ 34 | For \code{build_authorization_uri}, the authorization URI as a string. This can be set as a redirect from within a Shiny app's UI component. 35 | 36 | For \code{get_device_creds}, a list containing the following components: 37 | \itemize{ 38 | \item \code{user_code}: A short string to be shown to the user 39 | \item \code{device_code}: A long string to verify the session with the AAD server 40 | \item \code{verification_uri}: The URI the user should browse to in order to login 41 | \item \code{expires_in}: The duration in seconds for which the user and device codes are valid 42 | \item \code{interval}: The interval between polling requests to the AAD token endpoint 43 | \item \code{message}: A string with login instructions for the user 44 | } 45 | } 46 | \description{ 47 | Standalone OAuth authorization functions 48 | } 49 | \details{ 50 | These functions are mainly for use in embedded scenarios, such as within a Shiny web app. In this case, the interactive authentication flows (authorization code and device code) need to be split up so that the authorization step is handled separately from the token acquisition step. You should not need to use these functions inside a regular R session, or when executing an R batch script. 51 | } 52 | \examples{ 53 | build_authorization_uri("https://myresource", "mytenant", "app_id", 54 | redirect_uri="http://localhost:8100") 55 | 56 | \dontrun{ 57 | 58 | ## obtaining an authorization code separately to acquiring the token 59 | # first, get the authorization URI 60 | auth_uri <- build_authorization_uri("https://management.azure.com/", "mytenant", "app_id") 61 | # browsing to the URI will log you in and redirect to another URI containing the auth code 62 | browseURL(auth_uri) 63 | # use the code to acquire the token 64 | get_azure_token("https://management.azure.com/", "mytenant", "app_id", 65 | auth_code="code-from-redirect") 66 | 67 | 68 | ## obtaining device credentials separately to acquiring the token 69 | # first, contact the authorization endpoint to get the user and device codes 70 | creds <- get_device_creds("https://management.azure.com/", "mytenant", "app_id") 71 | # print the login instructions 72 | creds$message 73 | # use the creds to acquire the token 74 | get_azure_token("https://management.azure.com/", "mytenant", "app_id", 75 | auth_type="device_code", device_creds=creds) 76 | 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /man/cert_assertion.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/cert_creds.R 3 | \name{cert_assertion} 4 | \alias{cert_assertion} 5 | \title{Create a client assertion for certificate authentication} 6 | \usage{ 7 | cert_assertion(certificate, duration = 3600, signature_size = 256, ...) 8 | } 9 | \arguments{ 10 | \item{certificate}{An Azure Key Vault certificate object, or the name of a PEM or PFX file containing \emph{both} a private key and a public certificate.} 11 | 12 | \item{duration}{The requested validity period of the token, in seconds. The default is 1 hour.} 13 | 14 | \item{signature_size}{The size of the SHA2 signature.} 15 | 16 | \item{...}{Other named arguments which will be treated as custom claims.} 17 | } 18 | \value{ 19 | An object of S3 class \code{cert_assertion}, which is a list representing the assertion. 20 | } 21 | \description{ 22 | Create a client assertion for certificate authentication 23 | } 24 | \details{ 25 | Use this function to customise a client assertion for authenticating with a certificate. 26 | } 27 | \examples{ 28 | \dontrun{ 29 | 30 | cert_assertion("mycert.pem", duration=2*3600) 31 | cert_assertion("mycert.pem", custom_data="some text") 32 | 33 | # using a cert stored in Azure Key Vault 34 | cert <- AzureKeyVault::key_vault("myvault")$certificates$get("mycert") 35 | cert_assertion(cert, duration=2*3600) 36 | 37 | } 38 | } 39 | \seealso{ 40 | \link{get_azure_token} 41 | } 42 | -------------------------------------------------------------------------------- /man/figures/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudyr/AzureAuth/f6e54d77089d37fa42d437d6dc56c9917282c41a/man/figures/logo.png -------------------------------------------------------------------------------- /man/format.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/format.R 3 | \name{format_auth_header} 4 | \alias{format_auth_header} 5 | \title{Format an AzureToken object} 6 | \usage{ 7 | format_auth_header(token) 8 | } 9 | \arguments{ 10 | \item{token}{An Azure OAuth token.} 11 | } 12 | \description{ 13 | Format an AzureToken object 14 | } 15 | -------------------------------------------------------------------------------- /man/get_azure_token.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/managed_token.R, R/token.R 3 | \name{get_managed_token} 4 | \alias{get_managed_token} 5 | \alias{get_azure_token} 6 | \alias{delete_azure_token} 7 | \alias{load_azure_token} 8 | \alias{clean_token_directory} 9 | \alias{list_azure_tokens} 10 | \alias{token_hash} 11 | \alias{is_azure_token} 12 | \alias{is_azure_v1_token} 13 | \alias{is_azure_v2_token} 14 | \title{Manage Azure Active Directory OAuth 2.0 tokens} 15 | \usage{ 16 | get_managed_token(resource, token_args = list(), use_cache = NULL) 17 | 18 | get_azure_token( 19 | resource, 20 | tenant, 21 | app, 22 | password = NULL, 23 | username = NULL, 24 | certificate = NULL, 25 | auth_type = NULL, 26 | aad_host = "https://login.microsoftonline.com/", 27 | version = 1, 28 | authorize_args = list(), 29 | token_args = list(), 30 | use_cache = NULL, 31 | on_behalf_of = NULL, 32 | auth_code = NULL, 33 | device_creds = NULL 34 | ) 35 | 36 | delete_azure_token( 37 | resource, 38 | tenant, 39 | app, 40 | password = NULL, 41 | username = NULL, 42 | certificate = NULL, 43 | auth_type = NULL, 44 | aad_host = "https://login.microsoftonline.com/", 45 | version = 1, 46 | authorize_args = list(), 47 | token_args = list(), 48 | on_behalf_of = NULL, 49 | hash = NULL, 50 | confirm = TRUE 51 | ) 52 | 53 | load_azure_token(hash) 54 | 55 | clean_token_directory(confirm = TRUE) 56 | 57 | list_azure_tokens() 58 | 59 | token_hash( 60 | resource, 61 | tenant, 62 | app, 63 | password = NULL, 64 | username = NULL, 65 | certificate = NULL, 66 | auth_type = NULL, 67 | aad_host = "https://login.microsoftonline.com/", 68 | version = 1, 69 | authorize_args = list(), 70 | token_args = list(), 71 | on_behalf_of = NULL 72 | ) 73 | 74 | is_azure_token(object) 75 | 76 | is_azure_v1_token(object) 77 | 78 | is_azure_v2_token(object) 79 | } 80 | \arguments{ 81 | \item{resource}{For AAD v1.0, the URL of your resource host, or a GUID. For AAD v2.0, a character vector of scopes, each consisting of a URL or GUID along with a path designating the access scope. See 'Details' below.} 82 | 83 | \item{token_args}{An optional list of further parameters for the token endpoint. These will be included in the body of the request for \code{get_azure_token}, or as URI query parameters for \code{get_managed_token}.} 84 | 85 | \item{use_cache}{If TRUE and cached credentials exist, use them instead of obtaining a new token. The default value of NULL means to use the cache only if AzureAuth is not running inside a Shiny app.} 86 | 87 | \item{tenant}{Your tenant. This can be a name ("myaadtenant"), a fully qualified domain name ("myaadtenant.onmicrosoft.com" or "mycompanyname.com"), or a GUID. It can also be one of the generic tenants "common", "organizations" or "consumers"; see 'Generic tenants' below.} 88 | 89 | \item{app}{The client/app ID to use to authenticate with.} 90 | 91 | \item{password}{For most authentication flows, this is the password for the \emph{app} where needed, also known as the client secret. For the resource owner grant, this is your personal account password. See 'Details' below.} 92 | 93 | \item{username}{Your AAD username, if using the resource owner grant. See 'Details' below.} 94 | 95 | \item{certificate}{A file containing the certificate for authenticating with (including the private key), an Azure Key Vault certificate object, or a call to the \code{cert_assertion} function to build a client assertion with a certificate. See 'Certificate authentication' below.} 96 | 97 | \item{auth_type}{The authentication type. See 'Details' below.} 98 | 99 | \item{aad_host}{URL for your AAD host. For the public Azure cloud, this is \verb{https://login.microsoftonline.com/}. Change this if you are using a government or private cloud. Can also be a full URL, eg \verb{https://mydomain.b2clogin.com/mydomain/other/path/names/oauth2} (this is relevant mainly for Azure B2C logins).} 100 | 101 | \item{version}{The AAD version, either 1 or 2. Authenticating with a personal account as opposed to a work or school account requires AAD 2.0. The default is AAD 1.0 for compatibility reasons, but you should use AAD 2.0 if possible.} 102 | 103 | \item{authorize_args}{An optional list of further parameters for the AAD authorization endpoint. These will be included in the request URI as query parameters. Only used if \code{auth_type="authorization_code"}.} 104 | 105 | \item{on_behalf_of}{For the on-behalf-of authentication type, a token. This should be either an AzureToken object, or a string containing the JWT-encoded token itself.} 106 | 107 | \item{auth_code}{For the \code{authorization_code} flow, the code. Only used if \code{auth_type == "authorization_code"}.} 108 | 109 | \item{device_creds}{For the \code{device_code} flow, the device credentials used to verify the session between the client and the server. Only used if \code{auth_type == "device_code"}.} 110 | 111 | \item{hash}{The MD5 hash of this token, computed from the above inputs. Used by \code{load_azure_token} and \code{delete_azure_token} to identify a cached token to load and delete, respectively.} 112 | 113 | \item{confirm}{For \code{delete_azure_token}, whether to prompt for confirmation before deleting a token.} 114 | 115 | \item{object}{For \code{is_azure_token}, \code{is_azure_v1_token} and \code{is_azure_v2_token}, an R object.} 116 | } 117 | \description{ 118 | Use these functions to authenticate with Azure Active Directory (AAD). 119 | } 120 | \details{ 121 | \code{get_azure_token} does much the same thing as \code{\link[httr:oauth2.0_token]{httr::oauth2.0_token()}}, but customised for Azure. It obtains an OAuth token, first by checking if a cached value exists on disk, and if not, acquiring it from the AAD server. \code{load_azure_token} loads a token given its hash, \code{delete_azure_token} deletes a cached token given either the credentials or the hash, and \code{list_azure_tokens} lists currently cached tokens. 122 | 123 | \code{get_managed_token} is a specialised function to acquire tokens for a \emph{managed identity}. This is an Azure service, such as a VM or container, that has been assigned its own identity and can be granted access permissions like a regular user. The advantage of managed identities over the other authentication methods (see below) is that you don't have to store a secret password, which improves security. Note that \code{get_managed_token} can only be used from within the managed identity itself. 124 | 125 | By default \code{get_managed_token} retrieves a token using the system-assigned identity for the resource. To obtain a token with a user-assigned identity, pass either the client, object or Azure resource ID in the \code{token_args} argument. See the examples below. 126 | 127 | The \code{resource} arg should be a single URL or GUID for AAD v1.0. For AAD v2.0, it should be a vector of \emph{scopes}, where each scope consists of a URL or GUID along with a path that designates the type of access requested. If a v2.0 scope doesn't have a path, \code{get_azure_token} will append the \verb{/.default} path with a warning. A special scope is \code{offline_access}, which requests a refresh token from AAD along with the access token: without this scope, you will have to reauthenticate if you want to refresh the token. 128 | 129 | The \code{auth_code} and \code{device_creds} arguments are intended for use in embedded scenarios, eg when AzureAuth is loaded from within a Shiny web app. They enable the flow authorization step to be separated from the token acquisition step, which is necessary within an app; you can generally ignore these arguments when using AzureAuth interactively or as part of an R script. See the help for \link{build_authorization_uri} for examples on their use. 130 | 131 | \code{token_hash} computes the MD5 hash of its arguments. This is used by AzureAuth to identify tokens for caching purposes. Note that tokens are only cached if you allowed AzureAuth to create a data directory at package startup. 132 | 133 | One particular use of the \code{authorize_args} argument is to specify a different redirect URI to the default; see the examples below. 134 | } 135 | \section{Authentication methods}{ 136 | 137 | \enumerate{ 138 | \item Using the \strong{authorization_code} method is a multi-step process. First, \code{get_azure_token} opens a login window in your browser, where you can enter your AAD credentials. In the background, it loads the \href{https://github.com/rstudio/httpuv}{httpuv} package to listen on a local port. Once you have logged in, the AAD server redirects your browser to a local URL that contains an authorization code. \code{get_azure_token} retrieves this authorization code and sends it to the AAD access endpoint, which returns the OAuth token. 139 | \item The \strong{device_code} method is similar in concept to authorization_code, but is meant for situations where you are unable to browse the Internet -- for example if you don't have a browser installed or your computer has input constraints. First, \code{get_azure_token} contacts the AAD devicecode endpoint, which responds with a login URL and an access code. You then visit the URL and enter the code, possibly using a different computer. Meanwhile, \code{get_azure_token} polls the AAD access endpoint for a token, which is provided once you have entered the code. 140 | \item The \strong{client_credentials} method is much simpler than the above methods, requiring only one step. \code{get_azure_token} contacts the access endpoint, passing it either the app secret or the certificate assertion (which you supply in the \code{password} or \code{certificate} argument respectively). Once the credentials are verified, the endpoint returns the token. This is the method typically used by service accounts. 141 | \item The \strong{resource_owner} method also requires only one step. In this method, \code{get_azure_token} passes your (personal) username and password to the AAD access endpoint, which validates your credentials and returns the token. 142 | \item The \strong{on_behalf_of} method is used to authenticate with an Azure resource by passing a token obtained beforehand. It is mostly used by intermediate apps to authenticate for users. In particular, you can use this method to obtain tokens for multiple resources, while only requiring the user to authenticate once: see the examples below. 143 | } 144 | 145 | If the authentication method is not specified, it is chosen based on the presence or absence of the other arguments, and whether httpuv is installed. 146 | 147 | The httpuv package must be installed to use the authorization_code method, as this requires a web server to listen on the (local) redirect URI. See \link[httr:oauth2.0_token]{httr::oauth2.0_token} for more information; note that Azure does not support the \code{use_oob} feature of the httr OAuth 2.0 token class. 148 | 149 | Similarly, since the authorization_code method opens a browser to load the AAD authorization page, your machine must have an Internet browser installed that can be run from inside R. In particular, if you are using a Linux \href{https://azure.microsoft.com/en-us/services/virtual-machines/data-science-virtual-machines/}{Data Science Virtual Machine} in Azure, you may run into difficulties; use one of the other methods instead. 150 | } 151 | 152 | \section{Certificate authentication}{ 153 | 154 | OAuth tokens can be authenticated via an SSL/TLS certificate, which is considered more secure than a client secret. To do this, use the \code{certificate} argument, which can contain any of the following: 155 | \itemize{ 156 | \item The name of a PEM or PFX file, containing \emph{both} the private key and the public certificate. 157 | \item A certificate object from the AzureKeyVault package, representing a cert stored in the Key Vault service. 158 | \item A call to the \code{cert_assertion()} function to customise details of the requested token, eg the duration, expiry date, custom claims, etc. See the examples below. 159 | } 160 | } 161 | 162 | \section{Generic tenants}{ 163 | 164 | 165 | There are 3 generic values that can be used as tenants when authenticating:\tabular{ll}{ 166 | Tenant \tab Description \cr 167 | \code{common} \tab Allows users with both personal Microsoft accounts and work/school accounts from Azure AD to sign into the application. \cr 168 | \code{organizations} \tab Allows only users with work/school accounts from Azure AD to sign into the application. \cr 169 | \code{consumers} \tab Allows only users with personal Microsoft accounts (MSA) to sign into the application. \cr 170 | } 171 | } 172 | 173 | \section{Authentication vs authorization}{ 174 | 175 | Azure Active Directory can be used for two purposes: \emph{authentication} (verifying that a user is who they claim they are) and \emph{authorization} (granting a user permission to access a resource). In AAD, a successful authorization process concludes with the granting of an OAuth 2.0 access token, as discussed above. Authentication uses the same process but concludes by granting an ID token, as defined in the OpenID Connect protocol. 176 | 177 | \code{get_azure_token} can be used to obtain ID tokens along with regular OAuth access tokens, when using an interactive flow (authorization_code or device_code). The behaviour depends on the AAD version: 178 | 179 | When retrieving ID tokens, the behaviour depends on the AAD version: 180 | \itemize{ 181 | \item AAD v1.0 will return an ID token as well as the access token by default; you don't have to do anything extra. However, AAD v1.0 will not \emph{refresh} the ID token when it expires; you must reauthenticate to get a new one. To ensure you don't pull the cached version of the credentials, specify \code{use_cache=FALSE} in the calls to \code{get_azure_token}. 182 | \item Unlike AAD v1.0, AAD v2.0 does not return an ID token by default. To get a token, include \code{openid} as a scope. On the other hand it \emph{does} refresh the ID token, so bypassing the cache is not needed. It's recommended to use AAD v2.0 if you only want an ID token. 183 | } 184 | 185 | If you \emph{only} want to do authentication and not authorization (for example if your app does not use any Azure resources), specify the \code{resource} argument as follows: 186 | \itemize{ 187 | \item For AAD v1.0, use a blank resource (\code{resource=""}). 188 | \item For AAD v2.0, use \code{resource="openid"} without any other elements. Optionally you can add \code{"offline_access"} as a 2nd element if you want a refresh token as well. 189 | } 190 | 191 | See also the examples below. 192 | } 193 | 194 | \section{Caching}{ 195 | 196 | AzureAuth caches tokens based on all the inputs to \code{get_azure_token} as listed above. Tokens are cached in a custom, user-specific directory, created with the rappdirs package. On recent Windows versions, this will usually be in the location \verb{C:\\\\Users\\\\(username)\\\\AppData\\\\Local\\\\AzureR}. On Linux, it will be in \verb{~/.config/AzureR}, and on MacOS, it will be in \verb{~/Library/Application Support/AzureR}. Alternatively, you can specify the location of the directory in the environment variable \code{R_AZURE_DATA_DIR}. Note that a single directory is used for all tokens, and the working directory is not touched (which significantly lessens the risk of accidentally introducing cached tokens into source control). 197 | 198 | To list all cached tokens on disk, use \code{list_azure_tokens}. This returns a list of token objects, named according to their MD5 hashes. 199 | 200 | To delete a cached token, use \code{delete_azure_token}. This takes the same inputs as \code{get_azure_token}, or you can specify the MD5 hash directly in the \code{hash} argument. 201 | 202 | To delete all files in the caching directory, use \code{clean_token_directory}. 203 | } 204 | 205 | \section{Refreshing}{ 206 | 207 | A token object can be refreshed by calling its \code{refresh()} method. If the token's credentials contain a refresh token, this is used; otherwise a new access token is obtained by reauthenticating. 208 | 209 | Note that in AAD, a refresh token can be used to obtain an access token for any resource or scope that you have permissions for. Thus, for example, you could use a refresh token issued on a request for Azure Resource Manager (\verb{https://management.azure.com/}) to obtain a new access token for Microsoft Graph (\verb{https://graph.microsoft.com/}). 210 | 211 | To obtain an access token for a new resource, change the object's \code{resource} (for an AAD v1.0 token) or \code{scope} field (for an AAD v2.0 token) before calling \code{refresh()}. If you \emph{also} want to retain the token for the old resource, you should call the \code{clone()} method first to create a copy. See the examples below. 212 | } 213 | 214 | \section{Value}{ 215 | 216 | For \code{get_azure_token}, an object inheriting from \code{AzureToken}. The specific class depends on the authentication flow: \code{AzureTokenAuthCode}, \code{AzureTokenDeviceCode}, \code{AzureTokenClientCreds}, \code{AzureTokenOnBehalfOf}, \code{AzureTokenResOwner}. For \code{get_managed_token}, a similar object of class \code{AzureTokenManaged}. 217 | 218 | For \code{list_azure_tokens}, a list of such objects retrieved from disk. 219 | 220 | The actual credentials that are returned from the authorization endpoint can be found in the \code{credentials} field, the same as with a \code{httr::Token} object. The access token (if present) will be \code{credentials$access_token}, and the ID token (if present) will be \code{credentials$id_token}. Use these if you are manually constructing a HTTP request and need to insert an "Authorization" header, for example. 221 | } 222 | 223 | \examples{ 224 | \dontrun{ 225 | 226 | # authenticate with Azure Resource Manager: 227 | # no user credentials are supplied, so this will use the authorization_code 228 | # method if httpuv is installed, and device_code if not 229 | get_azure_token("https://management.azure.com/", tenant="mytenant", app="app_id") 230 | 231 | # you can force a specific authentication method with the auth_type argument 232 | get_azure_token("https://management.azure.com/", tenant="mytenant", app="app_id", 233 | auth_type="device_code") 234 | 235 | # to default to the client_credentials method, supply the app secret as the password 236 | get_azure_token("https://management.azure.com/", tenant="mytenant", app="app_id", 237 | password="app_secret") 238 | 239 | # authenticate to your resource with the resource_owner method: provide your username and password 240 | get_azure_token("https://myresource/", tenant="mytenant", app="app_id", 241 | username="user", password="abcdefg") 242 | 243 | # obtaining multiple tokens: authenticate (interactively) once... 244 | tok0 <- get_azure_token("serviceapp_id", tenant="mytenant", app="clientapp_id", 245 | auth_type="authorization_code") 246 | # ...then get tokens for each resource (Resource Manager and MS Graph) with on_behalf_of 247 | tok1 <- get_azure_token("https://management.azure.com/", tenant="mytenant", app="serviceapp_id", 248 | password="serviceapp_secret", on_behalf_of=tok0) 249 | tok2 <- get_azure_token("https://graph.microsoft.com/", tenant="mytenant", app="serviceapp_id", 250 | password="serviceapp_secret", on_behalf_of=tok0) 251 | 252 | 253 | # authorization_code flow with app registered in AAD as a web rather than a native client: 254 | # supply the client secret in the password arg 255 | get_azure_token("https://management.azure.com/", "mytenant", "app_id", 256 | password="app_secret", auth_type="authorization_code") 257 | 258 | 259 | # use a different redirect URI to the default localhost:1410 260 | get_azure_token("https://management.azure.com/", tenant="mytenant", app="app_id", 261 | authorize_args=list(redirect_uri="http://localhost:8000")) 262 | 263 | 264 | # request an AAD v1.0 token for Resource Manager (the default) 265 | token1 <- get_azure_token("https://management.azure.com/", "mytenant", "app_id") 266 | 267 | # same request to AAD v2.0, along with a refresh token 268 | token2 <- get_azure_token(c("https://management.azure.com/.default", "offline_access"), 269 | "mytenant", "app_id", version=2) 270 | 271 | # requesting multiple scopes (Microsoft Graph) with AAD 2.0 272 | get_azure_token(c("https://graph.microsoft.com/User.Read.All", 273 | "https://graph.microsoft.com/User.ReadWrite.All", 274 | "https://graph.microsoft.com/Directory.ReadWrite.All", 275 | "offline_access"), 276 | "mytenant", "app_id", version=2) 277 | 278 | 279 | # list saved tokens 280 | list_azure_tokens() 281 | 282 | # delete a saved token from disk 283 | delete_azure_token(resource="https://myresource/", tenant="mytenant", app="app_id", 284 | username="user", password="abcdefg") 285 | 286 | # delete a saved token by specifying its MD5 hash 287 | delete_azure_token(hash="7ea491716e5b10a77a673106f3f53bfd") 288 | 289 | 290 | # authenticating for B2C logins (custom AAD host) 291 | get_azure_token("https://mydomain.com", "mytenant", "app_id", "password", 292 | aad_host="https://mytenant.b2clogin.com/tfp/mytenant.onmicrosoft.com/custom/oauth2") 293 | 294 | 295 | # authenticating with a certificate 296 | get_azure_token("https://management.azure.com/", "mytenant", "app_id", 297 | certificate="mycert.pem") 298 | 299 | # authenticating with a certificate stored in Azure Key Vault 300 | cert <- AzureKeyVault::key_vault("myvault")$certificates$get("mycert") 301 | get_azure_token("https://management.azure.com/", "mytenant", "app_id", 302 | certificate=cert) 303 | 304 | # get a token valid for 2 hours (default is 1 hour) 305 | get_azure_token("https://management.azure.com/", "mytenant", "app_id", 306 | certificate=cert_assertion("mycert.pem", duration=2*3600)) 307 | 308 | 309 | # ID token with AAD v1.0 310 | # if you only want an ID token, set the resource to blank ("") 311 | tok <- get_azure_token("", "mytenant", "app_id", use_cache=FALSE) 312 | extract_jwt(tok, "id") 313 | 314 | # ID token with AAD v2.0 (recommended) 315 | tok2 <- get_azure_token(c("openid", "offline_access"), "mytenant", "app_id", version=2) 316 | extract_jwt(tok2, "id") 317 | 318 | 319 | # get a token from within a managed identity (VM, container or service) 320 | get_managed_token("https://management.azure.com/") 321 | 322 | # get a token from a managed identity, with a user-defined identity: 323 | # specify one of the identity's object_id, client_id and mi_res_id (Azure resource ID) 324 | # you can get these values via the Azure Portal or Azure CLI 325 | get_managed_token("https://management.azure.com/", token_args=list( 326 | mi_res_id="/subscriptions/zzzz-zzzz/resourceGroups/resgroupname/..." 327 | )) 328 | 329 | # use a refresh token from one resource to get an access token for another resource 330 | tok <- get_azure_token("https://myresource", "mytenant", "app_id") 331 | tok2 <- tok$clone() 332 | tok2$resource <- "https://anotherresource" 333 | tok2$refresh() 334 | 335 | # same for AAD v2.0 336 | tok <- get_azure_token(c("https://myresource/.default", "offline_access"), 337 | "mytenant", "app_id", version=2) 338 | tok2 <- tok$clone() 339 | tok2$scope <- c("https://anotherresource/.default", "offline_access") 340 | tok2$refresh() 341 | 342 | 343 | # manually adding auth header for a HTTP request 344 | tok <- get_azure_token("https://myresource", "mytenant", "app_id") 345 | header <- httr::add_headers(Authorization=paste("Bearer", tok$credentials$access_token)) 346 | httr::GET("https://myresource/path/for/call", header, ...) 347 | 348 | } 349 | } 350 | \seealso{ 351 | \link{AzureToken}, \link[httr:oauth2.0_token]{httr::oauth2.0_token}, \link[httr:Token-class]{httr::Token}, \link{cert_assertion}, 352 | \link{build_authorization_uri}, \link{get_device_creds} 353 | 354 | \href{https://docs.microsoft.com/en-us/azure/active-directory/develop/}{Azure Active Directory for developers}, 355 | \href{https://docs.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/overview}{Managed identities overview} 356 | \href{https://www.oauth.com/oauth2-servers/device-flow/token-request/}{Device code flow on OAuth.com}, 357 | \href{https://tools.ietf.org/html/rfc6749}{OAuth 2.0 RFC} for the gory details on how OAuth works 358 | } 359 | -------------------------------------------------------------------------------- /man/guid.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/normalize.R 3 | \name{normalize_tenant} 4 | \alias{normalize_tenant} 5 | \alias{normalize_guid} 6 | \alias{is_guid} 7 | \title{Normalize GUID and tenant values} 8 | \usage{ 9 | normalize_tenant(tenant) 10 | 11 | normalize_guid(x) 12 | 13 | is_guid(x) 14 | } 15 | \arguments{ 16 | \item{tenant}{For \code{normalize_tenant}, a string containing an Azure Active Directory tenant. This can be a name ("myaadtenant"), a fully qualified domain name ("myaadtenant.onmicrosoft.com" or "mycompanyname.com"), or a valid GUID.} 17 | 18 | \item{x}{For \code{is_guid}, a character string; for \code{normalize_guid}, a string containing a \emph{validly formatted} GUID.} 19 | } 20 | \value{ 21 | For \code{is_guid}, a logical vector indicating which values of \code{x} are validly formatted GUIDs. 22 | 23 | For \code{normalize_guid}, a vector of GUIDs in canonical format. If any values of \code{x} are not recognised as GUIDs, it throws an error. 24 | 25 | For \code{normalize_tenant}, the normalized tenant IDs or names. 26 | } 27 | \description{ 28 | These functions are used by \code{get_azure_token} to recognise and properly format tenant and app IDs. \code{is_guid} can also be used generically for identifying GUIDs/UUIDs in any context. 29 | } 30 | \details{ 31 | A tenant can be identified either by a GUID, or its name, or a fully-qualified domain name (FQDN). The rules for normalizing a tenant are: 32 | \enumerate{ 33 | \item If \code{tenant} is recognised as a valid GUID, return its canonically formatted value 34 | \item Otherwise, if it is a FQDN, return it 35 | \item Otherwise, if it is one of the generic tenants "common", "organizations" or "consumers", return it 36 | \item Otherwise, append ".onmicrosoft.com" to it 37 | } 38 | 39 | These functions are vectorised. See the link below for the GUID formats they accept. 40 | } 41 | \examples{ 42 | 43 | is_guid("72f988bf-86f1-41af-91ab-2d7cd011db47") # TRUE 44 | is_guid("{72f988bf-86f1-41af-91ab-2d7cd011db47}") # TRUE 45 | is_guid("72f988bf-86f1-41af-91ab-2d7cd011db47}") # FALSE (unmatched brace) 46 | is_guid("microsoft") # FALSE 47 | 48 | # all of these return the same value 49 | normalize_guid("72f988bf-86f1-41af-91ab-2d7cd011db47") 50 | normalize_guid("{72f988bf-86f1-41af-91ab-2d7cd011db47}") 51 | normalize_guid("(72f988bf-86f1-41af-91ab-2d7cd011db47)") 52 | normalize_guid("72f988bf86f141af91ab2d7cd011db47") 53 | 54 | normalize_tenant("microsoft") # returns 'microsoft.onmicrosoft.com' 55 | normalize_tenant("microsoft.com") # returns 'microsoft.com' 56 | normalize_tenant("72f988bf-86f1-41af-91ab-2d7cd011db47") # returns the GUID 57 | 58 | # vector arguments are accepted 59 | ids <- c("72f988bf-86f1-41af-91ab-2d7cd011db47", "72f988bf86f141af91ab2d7cd011db47") 60 | is_guid(ids) 61 | normalize_guid(ids) 62 | normalize_tenant(c("microsoft", ids)) 63 | 64 | } 65 | \seealso{ 66 | \link{get_azure_token} 67 | 68 | \href{https://docs.microsoft.com/en-us/dotnet/api/system.guid.parse}{Parsing rules for GUIDs in .NET}. \code{is_guid} and \code{normalize_guid} recognise the "N", "D", "B" and "P" formats. 69 | } 70 | -------------------------------------------------------------------------------- /man/jwt.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/jwt.R 3 | \name{decode_jwt} 4 | \alias{decode_jwt} 5 | \alias{decode_jwt.AzureToken} 6 | \alias{decode_jwt.Token} 7 | \alias{decode_jwt.character} 8 | \alias{extract_jwt} 9 | \alias{extract_jwt.AzureToken} 10 | \alias{extract_jwt.Token} 11 | \alias{extract_jwt.character} 12 | \title{Get raw access token (which is a JWT object)} 13 | \usage{ 14 | decode_jwt(token, ...) 15 | 16 | \method{decode_jwt}{AzureToken}(token, type = c("access", "id"), ...) 17 | 18 | \method{decode_jwt}{Token}(token, type = c("access", "id"), ...) 19 | 20 | \method{decode_jwt}{character}(token, ...) 21 | 22 | extract_jwt(token, ...) 23 | 24 | \method{extract_jwt}{AzureToken}(token, type = c("access", "id"), ...) 25 | 26 | \method{extract_jwt}{Token}(token, type = c("access", "id"), ...) 27 | 28 | \method{extract_jwt}{character}(token, ...) 29 | } 30 | \arguments{ 31 | \item{token}{A token object. This can be an object of class \code{AzureToken}, of class \code{httr::Token}, or a character string containing the encoded token.} 32 | 33 | \item{...}{Other arguments passed to methods.} 34 | 35 | \item{type}{For the \code{AzureToken} and \code{httr::Token} methods, the token to decode/retrieve: either the access token or ID token.} 36 | } 37 | \value{ 38 | For \code{extract_jwt}, the character string containing the encoded token, suitable for including in a HTTP query. For \code{decode_jwt}, a list containing up to 3 components: \code{header}, \code{payload} and \code{signature}. 39 | } 40 | \description{ 41 | Get raw access token (which is a JWT object) 42 | } 43 | \details{ 44 | An OAuth token is a \emph{JSON Web Token}, which is a set of base64URL-encoded JSON objects containing the token credentials along with an optional (opaque) verification signature. \code{decode_jwt} decodes the credentials into an R object so they can be viewed. \code{extract_jwt} extracts the credentials from an R object of class \code{AzureToken} or \code{httr::Token}. 45 | 46 | Note that \code{decode_jwt} does not touch the token signature or attempt to verify the credentials. You should not rely on the decoded information without verifying it independently. Passing the token itself to Azure is safe, as Azure will carry out its own verification procedure. 47 | } 48 | \seealso{ 49 | \href{https://jwt.io}{jwt.io}, the main JWT informational site 50 | 51 | \href{https://jwt.ms}{jwt.ms}, Microsoft site to decode and explain JWTs 52 | 53 | \href{https://en.wikipedia.org/wiki/JSON_Web_Token}{JWT Wikipedia entry} 54 | } 55 | -------------------------------------------------------------------------------- /tests/testthat.R: -------------------------------------------------------------------------------- 1 | library(testthat) 2 | library(AzureAuth) 3 | 4 | test_check("AzureAuth") 5 | -------------------------------------------------------------------------------- /tests/testthat/test00_normalize.R: -------------------------------------------------------------------------------- 1 | context("Normalize") 2 | 3 | test_that("normalize_tenant, normalize_guid work", 4 | { 5 | guid <- "abcdefab-1234-5678-9012-abcdefabcdef" 6 | expect_identical(normalize_guid(guid), guid) 7 | guid2 <- paste0("{", guid, "}") 8 | expect_identical(normalize_guid(guid2), guid) 9 | guid3 <- paste0("(", guid, ")") 10 | expect_identical(normalize_guid(guid3), guid) 11 | guid4 <- gsub("-", "", guid, fixed=TRUE) 12 | expect_identical(normalize_guid(guid4), guid) 13 | 14 | # improperly formatted GUID will be treated as a name 15 | guid5 <- paste0("(", guid) 16 | expect_false(is_guid(guid5)) 17 | expect_error(normalize_guid(guid5)) 18 | expect_identical(normalize_tenant(guid5), paste0(guid5, ".onmicrosoft.com")) 19 | 20 | expect_identical(normalize_tenant("common"), "common") 21 | expect_identical(normalize_tenant("mytenant"), "mytenant.onmicrosoft.com") 22 | expect_identical(normalize_tenant("mytenant.com"), "mytenant.com") 23 | # iterating normalize shouldn't change result 24 | expect_identical(normalize_tenant(normalize_tenant("mytenant")), "mytenant.onmicrosoft.com") 25 | 26 | # vector args 27 | expect_true(all(is_guid(c(guid, guid2)))) 28 | expect_identical(is_guid(c(guid, guid5)), c(TRUE, FALSE)) 29 | expect_identical(normalize_guid(c(guid, guid2)), c(guid, guid)) 30 | expect_error(normalize_guid(c(guid, guid5))) 31 | expect_identical(normalize_tenant(c("mytenant", guid)), c("mytenant.onmicrosoft.com", guid)) 32 | }) 33 | 34 | 35 | test_that("verify_v2_scope works", 36 | { 37 | expect_silent(AzureAuth:::verify_v2_scope("https://resource.com/.default")) 38 | 39 | # supported OpenID scope 40 | expect_silent(AzureAuth:::verify_v2_scope("offline_access")) 41 | 42 | # unsupported OpenID scope 43 | expect_error(AzureAuth:::verify_v2_scope("address")) 44 | 45 | # no scope path 46 | expect_warning(newscope <- AzureAuth:::verify_v2_scope("https://resource")) 47 | expect_equal(newscope, "https://resource/.default") 48 | expect_warning(newscope <- AzureAuth:::verify_v2_scope("https://resource/")) 49 | expect_equal(newscope, "https://resource/.default") 50 | 51 | # GUIDs 52 | expect_silent(AzureAuth:::verify_v2_scope("12345678901234567890123456789012/.default")) 53 | expect_warning(newscope <- AzureAuth:::verify_v2_scope("12345678901234567890123456789012")) 54 | expect_equal(newscope, "12345678901234567890123456789012/.default") 55 | expect_warning(newscope <- AzureAuth:::verify_v2_scope("12345678901234567890123456789012/")) 56 | expect_equal(newscope, "12345678901234567890123456789012/.default") 57 | 58 | # not a URI or GUID 59 | expect_error(AzureAuth:::verify_v2_scope("resource")) 60 | expect_error(AzureAuth:::verify_v2_scope("resource/.default")) 61 | }) 62 | -------------------------------------------------------------------------------- /tests/testthat/test02_jwt.R: -------------------------------------------------------------------------------- 1 | context("JWT") 2 | 3 | tenant <- Sys.getenv("AZ_TEST_TENANT_ID") 4 | app <- Sys.getenv("AZ_TEST_APP_ID") 5 | username <- Sys.getenv("AZ_TEST_USERNAME") 6 | password <- Sys.getenv("AZ_TEST_PASSWORD") 7 | native_app <- Sys.getenv("AZ_TEST_NATIVE_APP_ID") 8 | cert_app <- Sys.getenv("AZ_TEST_CERT_APP_ID") 9 | cert_file <- Sys.getenv("AZ_TEST_CERT_FILE") 10 | web_app <- Sys.getenv("AZ_TEST_WEB_APP_ID") 11 | web_app_pwd <- Sys.getenv("AZ_TEST_WEB_APP_PASSWORD") 12 | 13 | if(tenant == "" || app == "" || username == "" || password == "" || native_app == "" || 14 | cert_app == "" || cert_file == "" || web_app == "" || web_app_pwd == "") 15 | skip("Authentication tests skipped: ARM credentials not set") 16 | 17 | aut_hash <- Sys.getenv("AZ_TEST_AUT_HASH") 18 | ccd_hash <- Sys.getenv("AZ_TEST_CCD_HASH") 19 | dev_hash <- Sys.getenv("AZ_TEST_DEV_HASH") 20 | 21 | if(aut_hash == "" || ccd_hash == "" || dev_hash == "") 22 | skip("Authentication tests skipped: token hashes not set") 23 | 24 | if(system.file(package="httpuv") == "") 25 | skip("Authentication tests skipped: httpuv must be installed") 26 | 27 | # not a perfect test: will fail to detect Linux DSVM issue 28 | if(!interactive()) 29 | skip("Authentication tests skipped: must be an interactive session") 30 | 31 | 32 | test_that("JWT functions work", 33 | { 34 | suppressWarnings(file.remove(dir(AzureR_dir(), full.names=TRUE))) 35 | 36 | res <- "https://management.azure.com/" 37 | tok <- get_azure_token(res, tenant, native_app) 38 | 39 | decoded <- decode_jwt(tok) 40 | expect_type(decoded, "list") 41 | expect_identical(names(decoded), c("header", "payload", "signature")) 42 | 43 | extracted <- extract_jwt(tok) 44 | expect_type(extracted, "character") 45 | 46 | expect_identical(decoded, decode_jwt(extracted)) 47 | 48 | decoded_id <- decode_jwt(tok, "id") 49 | expect_type(decoded_id, "list") 50 | # v1.0 ID token from token endpoint is unsigned 51 | expect_identical(names(decoded_id), c("header", "payload")) 52 | 53 | extracted_id <- extract_jwt(tok, "id") 54 | expect_type(extracted_id, "character") 55 | 56 | expect_identical(decoded_id, decode_jwt(extracted_id)) 57 | }) 58 | 59 | 60 | test_that("JWT functions work with AAD v2.0", 61 | { 62 | res <- "https://management.azure.com/.default" 63 | tok <- get_azure_token(c(res, "openid"), tenant, native_app, version=2) 64 | 65 | decoded <- decode_jwt(tok) 66 | expect_type(decoded, "list") 67 | expect_identical(names(decoded), c("header", "payload", "signature")) 68 | 69 | extracted <- extract_jwt(tok) 70 | expect_type(extracted, "character") 71 | 72 | expect_identical(decoded, decode_jwt(extracted)) 73 | 74 | decoded_id <- decode_jwt(tok, "id") 75 | expect_type(decoded_id, "list") 76 | expect_identical(names(decoded_id), c("header", "payload", "signature")) 77 | 78 | extracted_id <- extract_jwt(tok, "id") 79 | expect_type(extracted_id, "character") 80 | 81 | expect_identical(decoded_id, decode_jwt(extracted_id)) 82 | }) 83 | -------------------------------------------------------------------------------- /tests/testthat/test10_v1_token.R: -------------------------------------------------------------------------------- 1 | context("v1.0 token") 2 | 3 | tenant <- Sys.getenv("AZ_TEST_TENANT_ID") 4 | app <- Sys.getenv("AZ_TEST_APP_ID") 5 | username <- Sys.getenv("AZ_TEST_USERNAME") 6 | password <- Sys.getenv("AZ_TEST_PASSWORD") 7 | native_app <- Sys.getenv("AZ_TEST_NATIVE_APP_ID") 8 | cert_app <- Sys.getenv("AZ_TEST_CERT_APP_ID") 9 | cert_file <- Sys.getenv("AZ_TEST_CERT_FILE") 10 | web_app <- Sys.getenv("AZ_TEST_WEB_APP_ID") 11 | web_app_pwd <- Sys.getenv("AZ_TEST_WEB_APP_PASSWORD") 12 | 13 | if(tenant == "" || app == "" || username == "" || password == "" || native_app == "" || 14 | cert_app == "" || cert_file == "" || web_app == "" || web_app_pwd == "") 15 | skip("Authentication tests skipped: ARM credentials not set") 16 | 17 | aut_hash <- Sys.getenv("AZ_TEST_AUT_HASH") 18 | ccd_hash <- Sys.getenv("AZ_TEST_CCD_HASH") 19 | dev_hash <- Sys.getenv("AZ_TEST_DEV_HASH") 20 | 21 | if(aut_hash == "" || ccd_hash == "" || dev_hash == "") 22 | skip("Authentication tests skipped: token hashes not set") 23 | 24 | if(system.file(package="httpuv") == "") 25 | skip("Authentication tests skipped: httpuv must be installed") 26 | 27 | # not a perfect test: will fail to detect Linux DSVM issue 28 | if(!interactive()) 29 | skip("Authentication tests skipped: must be an interactive session") 30 | 31 | test_that("v1.0 simple authentication works", 32 | { 33 | suppressWarnings(file.remove(dir(AzureR_dir(), full.names=TRUE))) 34 | 35 | res <- "https://management.azure.com/" 36 | 37 | # obtain new tokens 38 | aut_tok <- get_azure_token(res, tenant, native_app, auth_type="authorization_code") 39 | expect_true(is_azure_token(aut_tok)) 40 | expect_identical(aut_tok$hash(), aut_hash) 41 | expect_identical(res, decode_jwt(aut_tok)$payload$aud) 42 | 43 | ccd_tok <- get_azure_token(res, tenant, app, password=password) 44 | expect_true(is_azure_token(ccd_tok)) 45 | expect_identical(ccd_tok$hash(), ccd_hash) 46 | expect_identical(res, decode_jwt(ccd_tok)$payload$aud) 47 | 48 | dev_tok <- get_azure_token(res, tenant, native_app, auth_type="device_code") 49 | expect_true(is_azure_token(dev_tok)) 50 | expect_identical(dev_tok$hash(), dev_hash) 51 | expect_identical(res, decode_jwt(dev_tok)$payload$aud) 52 | 53 | aut_tok2 <- load_azure_token(aut_hash) 54 | expect_true(is_azure_token(aut_tok2)) 55 | expect_identical(aut_tok$credentials$access_token, aut_tok2$credentials$access_token) 56 | 57 | aut_expire <- as.numeric(aut_tok$credentials$expires_on) 58 | ccd_expire <- as.numeric(ccd_tok$credentials$expires_on) 59 | dev_expire <- as.numeric(dev_tok$credentials$expires_on) 60 | 61 | Sys.sleep(2) 62 | 63 | # refresh/reauthenticate 64 | aut_tok$refresh() 65 | ccd_tok$refresh() 66 | dev_tok$refresh() 67 | 68 | expect_true(as.numeric(aut_tok$credentials$expires_on) > aut_expire) 69 | expect_true(as.numeric(ccd_tok$credentials$expires_on) > ccd_expire) 70 | expect_true(as.numeric(dev_tok$credentials$expires_on) > dev_expire) 71 | 72 | # load cached tokens: should not get repeated login prompts/screens 73 | aut_tok2 <- get_azure_token(res, tenant, native_app, auth_type="authorization_code") 74 | expect_true(is_azure_token(aut_tok2)) 75 | expect_identical(aut_tok2$hash(), aut_hash) 76 | 77 | ccd_tok2 <- get_azure_token(res, tenant, app, password=password) 78 | expect_true(is_azure_token(ccd_tok2)) 79 | expect_identical(ccd_tok2$hash(), ccd_hash) 80 | 81 | dev_tok2 <- get_azure_token(res, tenant, native_app, auth_type="device_code") 82 | expect_true(is_azure_token(dev_tok2)) 83 | expect_identical(dev_tok2$hash(), dev_hash) 84 | 85 | # resource must be a single string 86 | expect_error(get_azure_token(c("res", "openid"), tenant, native_app)) 87 | 88 | expect_null(delete_azure_token(res, tenant, native_app, auth_type="authorization_code", confirm=FALSE)) 89 | expect_null(delete_azure_token(res, tenant, app, password=password, confirm=FALSE)) 90 | expect_null(delete_azure_token(res, tenant, native_app, auth_type="device_code", confirm=FALSE)) 91 | }) 92 | 93 | -------------------------------------------------------------------------------- /tests/testthat/test11_v1_token_misc.R: -------------------------------------------------------------------------------- 1 | context("v1.0 token other") 2 | 3 | tenant <- Sys.getenv("AZ_TEST_TENANT_ID") 4 | app <- Sys.getenv("AZ_TEST_APP_ID") 5 | username <- Sys.getenv("AZ_TEST_USERNAME") 6 | password <- Sys.getenv("AZ_TEST_PASSWORD") 7 | native_app <- Sys.getenv("AZ_TEST_NATIVE_APP_ID") 8 | cert_app <- Sys.getenv("AZ_TEST_CERT_APP_ID") 9 | cert_file <- Sys.getenv("AZ_TEST_CERT_FILE") 10 | web_app <- Sys.getenv("AZ_TEST_WEB_APP_ID") 11 | web_app_pwd <- Sys.getenv("AZ_TEST_WEB_APP_PASSWORD") 12 | userpwd <- Sys.getenv("AZ_TEST_USERPWD") 13 | admin_username <- Sys.getenv("AZ_TEST_ADMINUSERNAME") 14 | 15 | if(tenant == "" || app == "" || username == "" || password == "" || native_app == "" || 16 | cert_app == "" || cert_file == "" || web_app == "" || web_app_pwd == "" || userpwd == "") 17 | skip("Authentication tests skipped: ARM credentials not set") 18 | 19 | aut_hash <- Sys.getenv("AZ_TEST_AUT_HASH") 20 | ccd_hash <- Sys.getenv("AZ_TEST_CCD_HASH") 21 | dev_hash <- Sys.getenv("AZ_TEST_DEV_HASH") 22 | 23 | if(aut_hash == "" || ccd_hash == "" || dev_hash == "") 24 | skip("Authentication tests skipped: token hashes not set") 25 | 26 | if(system.file(package="httpuv") == "") 27 | skip("Authentication tests skipped: httpuv must be installed") 28 | 29 | # not a perfect test: will fail to detect Linux DSVM issue 30 | if(!interactive()) 31 | skip("Authentication tests skipped: must be an interactive session") 32 | 33 | suppressWarnings(file.remove(dir(AzureR_dir(), full.names=TRUE))) 34 | 35 | 36 | test_that("Providing optional args works", 37 | { 38 | res <- "https://management.azure.com/" 39 | 40 | # login hint 41 | aut_tok <- get_azure_token(res, tenant, native_app, username=admin_username, auth_type="authorization_code") 42 | expect_true(is_azure_token(aut_tok)) 43 | expect_identical(res, decode_jwt(aut_tok)$payload$aud) 44 | 45 | expect_null( 46 | delete_azure_token(res, tenant, native_app, username=admin_username, auth_type="authorization_code", 47 | confirm=FALSE)) 48 | }) 49 | 50 | 51 | test_that("Providing path in aad_host works", 52 | { 53 | res <- "https://management.azure.com/" 54 | aad_url <- file.path("https://login.microsoftonline.com", normalize_tenant(tenant), "oauth2") 55 | 56 | tok <- get_azure_token(res, tenant, app, password=password, aad_host=aad_url) 57 | expect_true(is_azure_token(tok)) 58 | expect_identical(res, decode_jwt(tok)$payload$aud) 59 | }) 60 | 61 | 62 | test_that("On-behalf-of flow works", 63 | { 64 | tok0 <- get_azure_token(app, tenant, native_app) 65 | expect_true(is_azure_token(tok0)) 66 | 67 | name0 <- decode_jwt(tok0)$payload$name 68 | expect_type(name0, "character") 69 | 70 | tok1 <- get_azure_token("https://graph.microsoft.com/", tenant, app, password, on_behalf_of=tok0) 71 | expect_true(is_azure_token(tok1)) 72 | expect_identical("https://graph.microsoft.com/", decode_jwt(tok1)$payload$aud) 73 | 74 | name1 <- decode_jwt(tok1)$payload$name 75 | expect_identical(name0, name1) 76 | 77 | expect_silent(tok1$refresh()) 78 | }) 79 | 80 | 81 | test_that("Certificate authentication works", 82 | { 83 | res <- "https://management.azure.com/" 84 | tok <- get_azure_token(res, tenant, cert_app, certificate=cert_file) 85 | expect_true(is_azure_token(tok)) 86 | expect_identical(res, decode_jwt(tok)$payload$aud) 87 | }) 88 | 89 | 90 | test_that("Standalone auth works", 91 | { 92 | res <- "https://management.azure.com/" 93 | 94 | auth_uri <- build_authorization_uri(res, tenant, native_app) 95 | code <- AzureAuth:::listen_for_authcode(auth_uri, "http://localhost:1410") 96 | tok <- get_azure_token(res, tenant, native_app, auth_code=code, use_cache=FALSE) 97 | expect_identical(tok$hash(), aut_hash) 98 | expect_identical(res, decode_jwt(tok)$payload$aud) 99 | 100 | creds <- get_device_creds(res, tenant, native_app) 101 | cat(creds$message, "\n") 102 | tok2 <- get_azure_token(res, tenant, native_app, auth_type="device_code", device_creds=creds, use_cache=FALSE) 103 | expect_identical(tok2$hash(), dev_hash) 104 | expect_identical(res, decode_jwt(tok2)$payload$aud) 105 | }) 106 | 107 | 108 | test_that("Webapp authentication works", 109 | { 110 | res <- "https://management.azure.com/" 111 | 112 | tok <- get_azure_token(res, tenant, web_app, password=web_app_pwd, auth_type="authorization_code") 113 | expect_true(is_azure_token(tok)) 114 | expect_identical(res, decode_jwt(tok)$payload$aud) 115 | 116 | tok2 <- get_azure_token(res, tenant, web_app, password=web_app_pwd) # client credentials 117 | expect_true(is_azure_token(tok2)) 118 | expect_identical(tok2$auth_type, "client_credentials") 119 | expect_identical(res, decode_jwt(tok2)$payload$aud) 120 | 121 | tok3 <- get_azure_token(res, tenant, web_app, password=web_app_pwd, username=admin_username, 122 | auth_type="authorization_code") 123 | expect_true(is_azure_token(tok2)) 124 | expect_identical(res, decode_jwt(tok3)$payload$aud) 125 | 126 | # web app expects client secret 127 | expect_error(get_azure_token(res, tenant, web_app)) 128 | }) 129 | 130 | 131 | test_that("Resource owner grant works", 132 | { 133 | res <- "https://management.azure.com/" 134 | 135 | tok <- get_azure_token(res, tenant, native_app, password=userpwd, username=username, auth_type="resource_owner") 136 | expect_true(is_azure_token(tok)) 137 | }) 138 | 139 | 140 | test_that("Refreshing with changed resource works", 141 | { 142 | res <- "https://management.azure.com/" 143 | 144 | tok <- get_azure_token(res, tenant, native_app) 145 | expect_identical(res, decode_jwt(tok)$payload$aud) 146 | 147 | tok$resource <- "https://graph.microsoft.com/" 148 | tok$refresh() 149 | expect_identical("https://graph.microsoft.com/", decode_jwt(tok)$payload$aud) 150 | }) 151 | -------------------------------------------------------------------------------- /tests/testthat/test20_v2_token.R: -------------------------------------------------------------------------------- 1 | context("v2.0 token") 2 | 3 | tenant <- Sys.getenv("AZ_TEST_TENANT_ID") 4 | app <- Sys.getenv("AZ_TEST_APP_ID") 5 | username <- Sys.getenv("AZ_TEST_USERNAME") 6 | password <- Sys.getenv("AZ_TEST_PASSWORD") 7 | native_app <- Sys.getenv("AZ_TEST_NATIVE_APP_ID") 8 | cert_app <- Sys.getenv("AZ_TEST_CERT_APP_ID") 9 | cert_file <- Sys.getenv("AZ_TEST_CERT_FILE") 10 | web_app <- Sys.getenv("AZ_TEST_WEB_APP_ID") 11 | web_app_pwd <- Sys.getenv("AZ_TEST_WEB_APP_PASSWORD") 12 | 13 | if(tenant == "" || app == "" || username == "" || password == "" || native_app == "" || 14 | cert_app == "" || cert_file == "" || web_app == "" || web_app_pwd == "") 15 | skip("Authentication tests skipped: ARM credentials not set") 16 | 17 | aut_hash <- Sys.getenv("AZ_TEST_AUT_HASH2") 18 | ccd_hash <- Sys.getenv("AZ_TEST_CCD_HASH2") 19 | dev_hash <- Sys.getenv("AZ_TEST_DEV_HASH2") 20 | 21 | if(aut_hash == "" || ccd_hash == "" || dev_hash == "") 22 | skip("Authentication tests skipped: token hashes not set") 23 | 24 | if(system.file(package="httpuv") == "") 25 | skip("Authentication tests skipped: httpuv must be installed") 26 | 27 | # not a perfect test: will fail to detect Linux DSVM issue 28 | if(!interactive()) 29 | skip("Authentication tests skipped: must be an interactive session") 30 | 31 | # should get 2 authcode and 2 devcode prompts here 32 | test_that("v2.0 simple authentication works", 33 | { 34 | suppressWarnings(file.remove(dir(AzureR_dir(), full.names=TRUE))) 35 | 36 | res <- "https://management.azure.com/.default" 37 | resbase <- "https://management.azure.com" 38 | 39 | # obtain new tokens 40 | aut_tok <- get_azure_token(res, tenant, native_app, auth_type="authorization_code", version=2) 41 | expect_true(is_azure_token(aut_tok)) 42 | expect_identical(aut_tok$hash(), aut_hash) 43 | expect_identical(resbase, decode_jwt(aut_tok)$payload$aud) 44 | 45 | ccd_tok <- get_azure_token(res, tenant, app, password=password, version=2) 46 | expect_true(is_azure_token(ccd_tok)) 47 | expect_identical(ccd_tok$hash(), ccd_hash) 48 | expect_identical(resbase, decode_jwt(ccd_tok)$payload$aud) 49 | 50 | dev_tok <- get_azure_token(res, tenant, native_app, auth_type="device_code", version=2) 51 | expect_true(is_azure_token(dev_tok)) 52 | expect_identical(dev_tok$hash(), dev_hash) 53 | expect_identical(resbase, decode_jwt(dev_tok)$payload$aud) 54 | 55 | aut_expire <- as.numeric(aut_tok$credentials$expires_on) 56 | ccd_expire <- as.numeric(ccd_tok$credentials$expires_on) 57 | dev_expire <- as.numeric(dev_tok$credentials$expires_on) 58 | 59 | Sys.sleep(2) 60 | 61 | # refresh (will have to reauthenticate for authcode and devcode) 62 | aut_tok$refresh() 63 | ccd_tok$refresh() 64 | dev_tok$refresh() 65 | 66 | expect_true(as.numeric(aut_tok$credentials$expires_on) > aut_expire) 67 | expect_true(as.numeric(ccd_tok$credentials$expires_on) > ccd_expire) 68 | expect_true(as.numeric(dev_tok$credentials$expires_on) > dev_expire) 69 | 70 | expect_null(delete_azure_token(res, tenant, native_app, auth_type="authorization_code", version=2, confirm=FALSE)) 71 | expect_null(delete_azure_token(res, tenant, app, password=password, version=2, confirm=FALSE)) 72 | expect_null(delete_azure_token(res, tenant, native_app, auth_type="device_code", version=2, confirm=FALSE)) 73 | }) 74 | 75 | 76 | # should only get 1 authcode and 1 devcode prompt here 77 | test_that("v2.0 refresh with offline scope works", 78 | { 79 | res <- "https://management.azure.com/.default" 80 | res2 <- "offline_access" 81 | resbase <- "https://management.azure.com" 82 | 83 | aut_tok <- get_azure_token(c(res, res2), tenant, native_app, auth_type="authorization_code", version=2) 84 | expect_true(!is_empty(aut_tok$credentials$refresh_token)) 85 | expect_identical(resbase, decode_jwt(aut_tok)$payload$aud) 86 | 87 | dev_tok <- get_azure_token(c(res, res2), tenant, native_app, auth_type="device_code", version=2) 88 | expect_true(!is_empty(dev_tok$credentials$refresh_token)) 89 | expect_identical(resbase, decode_jwt(dev_tok)$payload$aud) 90 | 91 | aut_expire <- as.numeric(aut_tok$credentials$expires_on) 92 | dev_expire <- as.numeric(dev_tok$credentials$expires_on) 93 | 94 | Sys.sleep(2) 95 | 96 | # refresh (should not have to reauthenticate) 97 | aut_tok$refresh() 98 | dev_tok$refresh() 99 | 100 | expect_true(as.numeric(aut_tok$credentials$expires_on) > aut_expire) 101 | expect_true(as.numeric(dev_tok$credentials$expires_on) > dev_expire) 102 | 103 | # load cached tokens: should not get repeated login prompts/screens 104 | aut_tok2 <- get_azure_token(c(res, res2), tenant, native_app, auth_type="authorization_code", version=2) 105 | expect_true(is_azure_token(aut_tok2)) 106 | 107 | dev_tok2 <- get_azure_token(c(res, res2), tenant, native_app, auth_type="device_code", version=2) 108 | expect_true(is_azure_token(dev_tok2)) 109 | 110 | expect_null( 111 | delete_azure_token(c(res, res2), tenant, native_app, auth_type="authorization_code", version=2, confirm=FALSE)) 112 | expect_null(delete_azure_token(c(res, res2), tenant, native_app, auth_type="device_code", version=2, confirm=FALSE)) 113 | }) 114 | 115 | -------------------------------------------------------------------------------- /tests/testthat/test21_v2_token_misc.R: -------------------------------------------------------------------------------- 1 | context("v2.0 token other") 2 | 3 | tenant <- Sys.getenv("AZ_TEST_TENANT_ID") 4 | app <- Sys.getenv("AZ_TEST_APP_ID") 5 | username <- Sys.getenv("AZ_TEST_USERNAME") 6 | password <- Sys.getenv("AZ_TEST_PASSWORD") 7 | native_app <- Sys.getenv("AZ_TEST_NATIVE_APP_ID") 8 | cert_app <- Sys.getenv("AZ_TEST_CERT_APP_ID") 9 | cert_file <- Sys.getenv("AZ_TEST_CERT_FILE") 10 | web_app <- Sys.getenv("AZ_TEST_WEB_APP_ID") 11 | web_app_pwd <- Sys.getenv("AZ_TEST_WEB_APP_PASSWORD") 12 | userpwd <- Sys.getenv("AZ_TEST_USERPWD") 13 | admin_username <- Sys.getenv("AZ_TEST_ADMINUSERNAME") 14 | 15 | if(tenant == "" || app == "" || username == "" || password == "" || native_app == "" || 16 | cert_app == "" || cert_file == "" || web_app == "" || web_app_pwd == "" || userpwd == "") 17 | skip("Authentication tests skipped: ARM credentials not set") 18 | 19 | aut_hash <- Sys.getenv("AZ_TEST_AUT_HASH2") 20 | ccd_hash <- Sys.getenv("AZ_TEST_CCD_HASH2") 21 | dev_hash <- Sys.getenv("AZ_TEST_DEV_HASH2") 22 | 23 | if(aut_hash == "" || ccd_hash == "" || dev_hash == "") 24 | skip("Authentication tests skipped: token hashes not set") 25 | 26 | if(system.file(package="httpuv") == "") 27 | skip("Authentication tests skipped: httpuv must be installed") 28 | 29 | # not a perfect test: will fail to detect Linux DSVM issue 30 | if(!interactive()) 31 | skip("Authentication tests skipped: must be an interactive session") 32 | 33 | suppressWarnings(file.remove(dir(AzureR_dir(), full.names=TRUE))) 34 | 35 | 36 | # should get 1 authcode screen here 37 | test_that("Providing optional args works", 38 | { 39 | res <- "https://management.azure.com/.default" 40 | resbase <- "https://management.azure.com" 41 | 42 | aut_tok <- get_azure_token(res, tenant, native_app, username=admin_username, auth_type="authorization_code", 43 | version=2) 44 | expect_true(is_azure_token(aut_tok)) 45 | expect_identical(resbase, decode_jwt(aut_tok)$payload$aud) 46 | 47 | expect_null( 48 | delete_azure_token(res, tenant, native_app, username=admin_username, auth_type="authorization_code", version=2, 49 | confirm=FALSE)) 50 | }) 51 | 52 | 53 | # should get a 'permissions requested' screen here 54 | test_that("Providing multiple scopes works", 55 | { 56 | scopes <- c(paste0("https://graph.microsoft.com/", 57 | c("User.Read.All", "Directory.Read.All", "Directory.AccessAsUser.All")), 58 | "offline_access") 59 | 60 | aut_tok <- get_azure_token(scopes, tenant, native_app, auth_type="authorization_code", version=2) 61 | expect_true(is_azure_token(aut_tok)) 62 | expect_identical("https://graph.microsoft.com", decode_jwt(aut_tok)$payload$aud) 63 | }) 64 | 65 | 66 | test_that("Dubious requests handled gracefully", 67 | { 68 | badres <- "resource" 69 | expect_error(get_azure_token(badres, tenant, app, password=password, version=2)) 70 | 71 | nopath <- "https://management.azure.com" 72 | expect_warning(tok <- get_azure_token(nopath, tenant, app, password=password, version=2)) 73 | expect_equal(tok$scope, "https://management.azure.com/.default") 74 | }) 75 | 76 | 77 | test_that("Providing path in aad_host works", 78 | { 79 | res <- "https://management.azure.com/.default" 80 | aad_url <- file.path("https://login.microsoftonline.com", normalize_tenant(tenant), "oauth2/v2.0") 81 | resbase <- "https://management.azure.com" 82 | 83 | tok <- get_azure_token(res, tenant, app, password=password, aad_host=aad_url, version=2) 84 | expect_true(is_azure_token(tok)) 85 | expect_identical(resbase, decode_jwt(tok)$payload$aud) 86 | }) 87 | 88 | 89 | test_that("On-behalf-of flow works", 90 | { 91 | res <- file.path(app, ".default") 92 | res2 <- "offline_access" 93 | 94 | tok0 <- get_azure_token(c(res, res2), tenant, native_app, version=2) 95 | expect_true(is_azure_token(tok0)) 96 | 97 | name0 <- decode_jwt(tok0$credentials$access_token)$payload$name 98 | expect_type(name0, "character") 99 | 100 | tok1 <- get_azure_token("https://graph.microsoft.com/.default", tenant, app, password, on_behalf_of=tok0, version=2) 101 | expect_true(is_azure_token(tok1)) 102 | expect_identical("https://graph.microsoft.com", decode_jwt(tok1)$payload$aud) 103 | 104 | name1 <- decode_jwt(tok1$credentials$access_token)$payload$name 105 | expect_identical(name0, name1) 106 | 107 | expect_silent(tok1$refresh()) 108 | }) 109 | 110 | 111 | test_that("Certificate authentication works", 112 | { 113 | res <- "https://management.azure.com/.default" 114 | resbase <- "https://management.azure.com" 115 | tok <- get_azure_token(res, tenant, cert_app, certificate=cert_file, version=2) 116 | expect_true(is_azure_token(tok)) 117 | expect_identical(resbase, decode_jwt(tok)$payload$aud) 118 | }) 119 | 120 | 121 | test_that("Standalone auth works", 122 | { 123 | res <- "https://management.azure.com/.default" 124 | resbase <- "https://management.azure.com" 125 | 126 | auth_uri <- build_authorization_uri(res, tenant, native_app, version=2) 127 | code <- AzureAuth:::listen_for_authcode(auth_uri, "http://localhost:1410") 128 | tok <- get_azure_token(res, tenant, native_app, version=2, auth_code=code, use_cache=FALSE) 129 | expect_identical(tok$hash(), aut_hash) 130 | expect_identical(resbase, decode_jwt(tok)$payload$aud) 131 | 132 | creds <- get_device_creds(res, tenant, native_app, version=2) 133 | cat(creds$message, "\n") 134 | tok2 <- get_azure_token(res, tenant, native_app, auth_type="device_code", version=2, device_creds=creds, 135 | use_cache=FALSE) 136 | expect_identical(tok2$hash(), dev_hash) 137 | expect_identical(resbase, decode_jwt(tok2)$payload$aud) 138 | }) 139 | 140 | 141 | test_that("Webapp authentication works", 142 | { 143 | res <- "https://management.azure.com/.default" 144 | resbase <- "https://management.azure.com" 145 | 146 | tok <- get_azure_token(res, tenant, web_app, password=web_app_pwd, auth_type="authorization_code", version=2) 147 | expect_true(is_azure_token(tok)) 148 | expect_identical(resbase, decode_jwt(tok)$payload$aud) 149 | 150 | tok2 <- get_azure_token(res, tenant, web_app, password=web_app_pwd, version=2) # client credentials 151 | expect_true(is_azure_token(tok2)) 152 | expect_identical(tok2$auth_type, "client_credentials") 153 | expect_identical(resbase, decode_jwt(tok2)$payload$aud) 154 | 155 | tok3 <- get_azure_token(res, tenant, web_app, password=web_app_pwd, username=admin_username, 156 | auth_type="authorization_code", version=2) 157 | expect_true(is_azure_token(tok2)) 158 | expect_identical(resbase, decode_jwt(tok3)$payload$aud) 159 | 160 | # web app expects client secret 161 | expect_error(get_azure_token(res, tenant, web_app, version=2)) 162 | }) 163 | 164 | 165 | test_that("Resource owner grant works", 166 | { 167 | res <- "https://management.azure.com/.default" 168 | resbase <- "https://management.azure.com" 169 | 170 | tok <- get_azure_token(res, tenant, native_app, password=userpwd, username=username, auth_type="resource_owner", 171 | version=2) 172 | expect_true(is_azure_token(tok)) 173 | expect_identical(resbase, decode_jwt(tok)$payload$aud) 174 | }) 175 | 176 | 177 | test_that("Refreshing with changed resource works", 178 | { 179 | res <- "https://management.azure.com/.default" 180 | resbase <- "https://management.azure.com" 181 | res2 <- "offline_access" 182 | 183 | tok <- get_azure_token(c(res, res2), tenant, native_app, version=2) 184 | expect_identical(resbase, decode_jwt(tok)$payload$aud) 185 | 186 | tok$scope[1] <- "https://graph.microsoft.com/.default" 187 | tok$refresh() 188 | expect_identical(decode_jwt(tok)$payload$aud, "https://graph.microsoft.com") 189 | }) 190 | 191 | 192 | test_that("Consumers tenant works", 193 | { 194 | res <- "https://graph.microsoft.com/.default" 195 | res2 <- "offline_access" 196 | res3 <- "openid" 197 | 198 | tok <- get_azure_token(c(res, res2, res3), "consumers", native_app, version=2) 199 | expect_error(decode_jwt(tok)) 200 | expect_identical(decode_jwt(tok, "id")$payload$tid, "9188040d-6c67-4c5b-b112-36a304b66dad") 201 | }) 202 | -------------------------------------------------------------------------------- /vignettes/images/authcode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudyr/AzureAuth/f6e54d77089d37fa42d437d6dc56c9917282c41a/vignettes/images/authcode.png -------------------------------------------------------------------------------- /vignettes/images/clientcred.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudyr/AzureAuth/f6e54d77089d37fa42d437d6dc56c9917282c41a/vignettes/images/clientcred.png -------------------------------------------------------------------------------- /vignettes/images/devicecode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudyr/AzureAuth/f6e54d77089d37fa42d437d6dc56c9917282c41a/vignettes/images/devicecode.png -------------------------------------------------------------------------------- /vignettes/scenarios.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Common authentication scenarios" 3 | author: Hong Ooi 4 | output: rmarkdown::html_vignette 5 | vignette: > 6 | %\VignetteIndexEntry{Authentication scenarios} 7 | %\VignetteEngine{knitr::rmarkdown} 8 | %\VignetteEncoding{utf8} 9 | --- 10 | 11 | The interaction between app registration configuration and requesting a token can be confusing. This vignette outlines some common authentication scenarios that R users might encounter. For each scenario, we briefly describe the necessary settings for your app registration in Azure, and how to request a token using AzureAuth. The [Azure Active Directory documentation](https://docs.microsoft.com/en-au/azure/active-directory/develop/v2-overview) is the authoritative reference. 12 | 13 | ## Interactive authentication on a local machine 14 | 15 | This is the simplest scenario: you're using R on your local desktop or laptop, and you want to authenticate to Azure with your user credentials. 16 | 17 | The authentication flow to use in this case is **authorization_code**. This requires that you have a browser installed on your machine, and it can be called from within R (as is usually the case). You'll also need to have the [httpuv](https://cran.r-project.org/package=httpuv) package: this will be installed if you're a Shiny developer, but otherwise you might have to install it manually. 18 | 19 | The code to run in your R session is 20 | 21 | ```r 22 | library(AzureAuth) 23 | 24 | # for an AADv1 token 25 | tok <- get_azure_token("https://resource", tenant="yourtenant", app="yourappid") 26 | 27 | # for an AADv2 token 28 | tok <- get_azure_token("https://resource/scope", tenant="yourtenant", app="yourappid", version=2) 29 | ``` 30 | 31 | where `resource[/scope]` is the Azure resource/scope you want a token for, `yourtenant` is your Azure tenant name or GUID, and `yourappid` is your app registration ID. Note that you do _not_ specify your username or password in the `get_azure_token` call. 32 | 33 | On the server side, the app registration you use should have a **mobile & desktop redirect** of `http://localhost:1410`. See the crop below of the authentication pane for the app in the Azure portal. 34 | 35 | ![](images/authcode.png) 36 | 37 | 38 | ## Interactive authentication in a remote session 39 | 40 | This is the scenario where you are using R in a remote terminal session of some kind: RStudio Server, Azure Databricks, or a Linux VM over ssh. Here, you still want to authenticate with your user credentials, but a browser may not be available to use the regular AAD authentication process. 41 | 42 | The authentication flow to use is **device_code**. This requires that you have a browser available elsewhere (for example, on the local machine from which you're logged in to your remote session). The code to run in your R session is 43 | 44 | ```r 45 | tok <- get_azure_token("resource", tenant="yourtenant", app="yourappid", auth_type="device_code") 46 | ``` 47 | 48 | As before, you do _not_ include your username or password in the `get_azure_token` call. 49 | 50 | On the server side, the app registration should have the **"Allow public client flows"** setting enabled. 51 | 52 | ![](images/devicecode.png) 53 | 54 | It's possible, and indeed desirable, to combine this with the previous redirect URI in the one app registration. This way, you can use the same app ID to authenticate both locally and in a remote terminal. 55 | 56 | 57 | ## Interactive authentication in a webapp 58 | 59 | This is the scenario where you want to authenticate as part of a webapp, such as a Shiny app. 60 | 61 | For this scenario, your app registration should have a **webapp redirect** that has the same URL as your app, eg `https://youraccount.shinyapps.io/yourapp` for an app hosted in shinyapps.io. The difference between a mobile & desktop and a webapp redirect is that you supply a **client secret** when authenticating with the latter, but not the former. The client secret is like a password, and helps prevent third parties from hijacking your app registration. 62 | 63 | The authentication flow to use is **authorization_code**, the same as for a local machine. The Shiny R code that is required is described in more detail in the 'Authenticating from Shiny' vignette, but essentially, the authentication is split into 2 parts: the authorization step in the UI, and then the token acquisition step on the server. 64 | 65 | ```r 66 | tenant <- "yourtenant" 67 | app <- "yourappid" 68 | redirect <- "https://yourwebsite.example.com/" 69 | 70 | # authorization: part of UI.r 71 | auth_uri <- build_authorization_uri("https://resource", tenant, app, redirect_uri=redirect) 72 | redir_js <- sprintf("location.replace(\"%s\");", auth_uri) 73 | tags$script(HTML(redir_js)) 74 | 75 | # token acquisition: part of server.R 76 | tok <- get_azure_token("https://resource", tenant, app, password="client_secret", 77 | auth_type="authorization_code", authorize_args=list(redirect_uri=redirect), 78 | use_cache=FALSE, auth_code=opts$code) 79 | ``` 80 | 81 | Note that the `password` argument holds the client secret for the app registration, _not_ a user password. See the crop below of the certificates & secrets pane for the app registration. 82 | 83 | ![](images/clientcred.png) 84 | 85 | Be aware that the client secret is automatically generated by the server and cannot be modified. You can only see it at the time of generation, so make sure you note down its value. 86 | 87 | 88 | ## Non-interactive authentication 89 | 90 | This is the scenario where you want to authenticate to Azure without a user account present, for example in a deployment pipeline. 91 | 92 | The authentication flow to use is **client_credentials**. The code to run looks like 93 | 94 | ```r 95 | tok <- get_azure_token("resource", tenant="yourtenant", app="yourccappid", password="client_secret") 96 | ``` 97 | 98 | Here, `yourccappid` is the app ID to use for your pipeline; you should not use the same app registration for this purpose as for interactive logins. The `password` argument is the **client secret** for your app registration, and not a user password. The client secret is set in the same way as for the interactive webapp scenario, above. 99 | 100 | As an alternative to a client secret, it's possible to authenticate using a TLS certificate (public key). This is considered more secure, but is also more complicated to setup. For more information, see the AAD docs linked previously. 101 | 102 | 103 | ## More information 104 | 105 | The following pages at the AAD documentation will be helpful if you're new to creating app registrations: 106 | 107 | - [A step-by-step guide](https://docs.microsoft.com/en-us/azure/active-directory/develop/quickstart-register-app) to registering an app in the Azure portal. 108 | 109 | - [How to set permissions for an app](https://docs.microsoft.com/en-us/azure/active-directory/develop/quickstart-configure-app-access-web-apis). For interactive authentication (authenticating as a user), you want _delegated_ permissions. For non-interactive authentication (authenticating as the application), you want _application_ permissions. 110 | 111 | - [Restricting your app to a set of users](https://docs.microsoft.com/en-us/azure/active-directory/develop/howto-restrict-your-app-to-a-set-of-users)---if you don't want your app to be accessible to every user in your organisation. Only applies to webapps, not native (desktop & mobile) apps. 112 | -------------------------------------------------------------------------------- /vignettes/shiny.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Authenticating from Shiny" 3 | author: Hong Ooi 4 | output: rmarkdown::html_vignette 5 | vignette: > 6 | %\VignetteIndexEntry{Shiny} 7 | %\VignetteEngine{knitr::rmarkdown} 8 | %\VignetteEncoding{utf8} 9 | --- 10 | 11 | Because a Shiny app has separate UI and server components, the interactive authentication flows require some changes. In particular, the authorization step (logging in to Azure) has to be conducted separately from the token acquisition step. 12 | 13 | AzureAuth provides the `build_authorization_uri` function to facilitate this separation. You call this function to obtain a URI that you browse to in order to login to Azure. Once you have logged in, Azure will return an authorization code as part of a redirect. 14 | 15 | Here is a skeleton Shiny app that demonstrates its use. The UI calls `build_authorization_uri`, and then redirects your browser to that location. When you have logged in, the server captures the authorization code and calls `get_azure_token` to obtain the token. Once the token is obtained, `shinyjs` is used to return the URL back to its original state. 16 | 17 | ```r 18 | library(AzureAuth) 19 | library(shiny) 20 | library(shinyjs) 21 | 22 | tenant <- "your-tenant-here" 23 | app <- "your-app-id-here" 24 | 25 | # the Azure resource permissions needed 26 | # if your app doesn't use any Azure resources (you only want to do authentication), 27 | # set the resource to "openid" only 28 | resource <- c("https://management.azure.com/.default", "openid") 29 | 30 | # set this to the site URL of your app once it is deployed 31 | # this must also be the redirect for your registered app in Azure Active Directory 32 | redirect <- "http://localhost:8100" 33 | 34 | port <- httr::parse_url(redirect)$port 35 | options(shiny.port=if(is.null(port)) 80 else as.numeric(port)) 36 | 37 | # replace this with your app's regular UI 38 | ui <- fluidPage( 39 | useShinyjs(), 40 | verbatimTextOutput("token") 41 | ) 42 | 43 | ui_func <- function(req) 44 | { 45 | opts <- parseQueryString(req$QUERY_STRING) 46 | if(is.null(opts$code)) 47 | { 48 | auth_uri <- build_authorization_uri(resource, tenant, app, redirect_uri=redirect, version=2) 49 | redir_js <- sprintf("location.replace(\"%s\");", auth_uri) 50 | tags$script(HTML(redir_js)) 51 | } 52 | else ui 53 | } 54 | 55 | # code for cleaning url after authentication 56 | clean_url_js <- sprintf( 57 | " 58 | $(document).ready(function(event) { 59 | const nextURL = '%s'; 60 | const nextTitle = 'My new page title'; 61 | const nextState = { additionalInformation: 'Updated the URL with JS' }; 62 | // This will create a new entry in the browser's history, without reloading 63 | window.history.pushState(nextState, nextTitle, nextURL); 64 | }); 65 | ", redirect 66 | ) 67 | 68 | server <- function(input, output, session) 69 | { 70 | shinyjs::runjs(clean_url_js) 71 | 72 | opts <- parseQueryString(isolate(session$clientData$url_search)) 73 | if(is.null(opts$code)) 74 | return() 75 | 76 | # this assumes your app has a 'public client/native' redirect: 77 | # if it is a 'web' redirect, include the client secret as the password argument 78 | token <- get_azure_token(resource, tenant, app, auth_type="authorization_code", 79 | authorize_args=list(redirect_uri=redirect), version=2, 80 | use_cache=FALSE, auth_code=opts$code) 81 | 82 | output$token <- renderPrint(token) 83 | } 84 | 85 | shinyApp(ui_func, server) 86 | ``` 87 | 88 | Note that this process is only necessary within a web app, and only when using an interactive authentication flow. In a normal R session, or when using the client credentials or resource owner grant flows, you can simply call `get_azure_token` directly. 89 | 90 | -------------------------------------------------------------------------------- /vignettes/token.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Acquire an OAuth token" 3 | author: Hong Ooi 4 | output: rmarkdown::html_vignette 5 | vignette: > 6 | %\VignetteIndexEntry{Acquire an OAuth token} 7 | %\VignetteEngine{knitr::rmarkdown} 8 | %\VignetteEncoding{utf8} 9 | --- 10 | 11 | This is a short introduction to authenticating with Azure Active Directory (AAD) with AzureAuth. 12 | 13 | ## The `get_azure_token` function 14 | 15 | The main function in AzureAuth is `get_azure_token`, which obtains an OAuth token from AAD: 16 | 17 | ```r 18 | library(AzureAuth) 19 | 20 | token <- get_azure_token(resource="myresource", tenant="mytenant", app="app_id", 21 | password="mypassword", username="username", certificate="encoded_cert", version=1, ...) 22 | ``` 23 | 24 | The function has the following arguments: 25 | 26 | - `resource`: The resource or scope for which you want a token. For AAD v1.0, this should be a single URL (eg "[https://example.com](https://example.com)") or a GUID. For AAD v2.0, this should be a vector of scopes (see below). 27 | - `tenant`: The AAD tenant. 28 | - `app`: The app ID or service principal ID to authenticate with. 29 | - `username`, `password`, `certificate`: Your authentication credentials. 30 | - `auth_type`: The OAuth authentication method to use. See the next section. 31 | - `version`: The version of AAD for which you want a token, either 1 or 2. The default is version 1. Note that the _OAuth scheme_ is always 2.0. 32 | 33 | Scopes in AAD v2.0 consist of a URL or a GUID, along with a path that designates the scope. If a scope doesn't have a path, `get_azure_token` will append the `/.default` path with a warning. A special scope is `offline_access`, which requests a refresh token from AAD along with the access token: without this, you will have to reauthenticate if you want to refresh the token. 34 | 35 | ```r 36 | # request an AAD v1.0 token for Resource Manager 37 | token1 <- get_azure_token("https://management.azure.com/", "mytenant", "app_id") 38 | 39 | # same request to AAD v2.0, along with a refresh token 40 | token2 <- get_azure_token(c("https://management.azure.com/.default", "offline_access"), 41 | "mytenant", "app_id", version=2) 42 | 43 | # requesting multiple scopes in AAD v2.0 (Microsoft Graph) 44 | scopes <- c("https://graph.microsoft.com/User.Read", 45 | "https://graph.microsoft.com/Files.Read", 46 | "https://graph.microsoft.com/Mail.Read", 47 | "offline_access") 48 | token3 <- get_azure_token(scopes, "mytenant", "app_id", version=2) 49 | ``` 50 | 51 | ## Authentication methods 52 | 53 | AzureAuth supports the following methods for authenticating with AAD: **authorization_code**, **device_code**, **client_credentials**, **resource_owner** and **on_behalf_of**. 54 | 55 | 1. Using the **authorization_code** method is a multi-step process. First, `get_azure_token` opens a login window in your browser, where you can enter your AAD credentials. In the background, it loads the [httpuv](https://github.com/rstudio/httpuv) package to listen on a local port. Once you have logged in, the AAD server redirects your browser to a local URL that contains an authorization code. `get_azure_token` retrieves this authorization code and sends it to the AAD access endpoint, which returns the OAuth token.

56 | The httpuv package must be installed to use this method, as it requires a web server to listen on the (local) redirect URI. Since it opens a browser to load the AAD authorization page, your machine must also have an Internet browser installed that can be run from inside R. In particular, if you are using a Linux [Data Science Virtual Machine](https://azure.microsoft.com/en-us/services/virtual-machines/data-science-virtual-machines/) in Azure, you may run into difficulties; use one of the other methods instead. 57 | 58 | ```r 59 | # obtain a token using authorization_code 60 | # no user credentials needed 61 | get_azure_token("myresource", "mytenant", "app_id", auth_type="authorization_code") 62 | ``` 63 | 64 | 2. The **device_code** method is similar in concept to authorization_code, but is meant for situations where you are unable to browse the Internet -- for example if you don't have a browser installed or your computer has input constraints. First, `get_azure_token` contacts the AAD devicecode endpoint, which responds with a login URL and an access code. You then visit the URL and enter the code, possibly using a different computer. Meanwhile, `get_azure_token` polls the AAD access endpoint for a token, which is provided once you have entered the code. 65 | 66 | ```r 67 | # obtain a token using device_code 68 | # no user credentials needed 69 | get_azure_token("myresource", "mytenant", "app_id", auth_type="device_code") 70 | ``` 71 | 72 | 3. The **client_credentials** method is much simpler than the above methods, requiring only one step. `get_azure_token` contacts the access endpoint, passing it the credentials. This can be either a client secret or a certificate, which you supply in the `password` or `certificate` argument respectively. Once the credentials are verified, the endpoint returns the token. This is the method typically used by service accounts. 73 | 74 | ```r 75 | # obtain a token using client_credentials 76 | # supply credentials in password arg 77 | get_azure_token("myresource", "mytenant", "app_id", 78 | password="client_secret", auth_type="client_credentials") 79 | 80 | # can also supply a client certificate as a PEM/PFX file... 81 | get_azure_token("myresource", "mytenant", "app_id", 82 | certificate="mycert.pem", auth_type="client_credentials") 83 | 84 | # ... or as an object in Azure Key Vault 85 | cert <- AzureKeyVault::key_vault("myvault")$certificates$get("mycert") 86 | get_azure_token("myresource", "mytenant", "app_id", 87 | certificate=cert, auth_type="client_credentials") 88 | ``` 89 | 90 | 4. The **resource_owner** method also requires only one step. In this method, `get_azure_token` passes your (personal) username and password to the AAD access endpoint, which validates your credentials and returns the token. 91 | 92 | ```r 93 | # obtain a token using resource_owner 94 | # supply credentials in username and password args 95 | get_azure_token("myresource", "mytenant", "app_id", 96 | username="myusername", password="mypassword", auth_type="resource_owner") 97 | ``` 98 | 99 | 5. The **on_behalf_of** method is used to authenticate with an Azure resource by passing a token obtained beforehand. It is mostly used by intermediate apps to authenticate for users. In particular, you can use this method to obtain tokens for multiple resources, while only requiring the user to authenticate once. 100 | 101 | ```r 102 | # obtaining multiple tokens: authenticate (interactively) once... 103 | tok0 <- get_azure_token("serviceapp_id", "mytenant", "clientapp_id", auth_type="authorization_code") 104 | # ...then get tokens for each resource with on_behalf_of 105 | tok1 <- get_azure_token("resource1", "mytenant," "serviceapp_id", 106 | password="serviceapp_secret", auth_type="on_behalf_of", on_behalf_of=tok0) 107 | tok2 <- get_azure_token("resource2", "mytenant," "serviceapp_id", 108 | password="serviceapp_secret", auth_type="on_behalf_of", on_behalf_of=tok0) 109 | ``` 110 | 111 | If you don't specify the method, `get_azure_token` makes a best guess based on the presence or absence of the other authentication arguments, and whether httpuv is installed. 112 | 113 | ```r 114 | # this will default to authorization_code if httpuv is installed, and device_code if not 115 | get_azure_token("myresource", "mytenant", "app_id") 116 | 117 | # this will use client_credentials method 118 | get_azure_token("myresource", "mytenant", "app_id", 119 | password="client_secret") 120 | 121 | # this will use on_behalf_of method 122 | get_azure_token("myresource", "mytenant", "app_id", 123 | password="client_secret", on_behalf_of=token) 124 | ``` 125 | 126 | ### Managed identities 127 | 128 | AzureAuth provides `get_managed_token` to obtain tokens from within a managed identity. This is a VM, service or container in Azure that can authenticate as itself, which removes the need to save secret passwords or certificates. 129 | 130 | ```r 131 | # run this from within an Azure VM or container for which an identity has been setup 132 | get_managed_token("myresource") 133 | ``` 134 | 135 | ### Inside a web app 136 | 137 | Using the interactive flows (authorization_code and device_code) from within a Shiny app requires separating the authorization (logging in to Azure) step from the token acquisition step. For this purpose, AzureAuth provides the `build_authorization_uri` and `get_device_creds` functions. You can use these from within your app to carry out the authorization, and then pass the resulting credentials to `get_azure_token` itself. See the "Authenticating from Shiny" vignette for an example app. 138 | 139 | ## Authentication vs authorization: 140 | 141 | Azure Active Directory can be used for two purposes: _authentication_ (verifying that a user is who they claim they are) and _authorization_ (granting a user permission to access a resource). In AAD, a successful authorization process concludes with the granting of an OAuth 2.0 access token, as discussed above. Authentication uses the same process but concludes by granting an ID token, as defined in the OpenID Connect protocol. 142 | 143 | You can use `get_azure_token` to obtain ID tokens, in addition to access tokens. 144 | 145 | With AAD v1.0, using an interactive authentication flow (authorization_code or device_code) will return an ID token by default -- you don't have to do anything extra. However, AAD v1.0 will _not_ refresh the ID token when it expires (only the access token). Because of this, specify `use_cache=FALSE` to avoid picking up cached token credentials which may have been refreshed previously. 146 | 147 | AAD v2.0 does not return an ID token by default, but you can get one by specifying `openid` as a scope. Again, this applies only to interactive authentication. If you only want an ID token, it's recommended to use AAD v2.0. 148 | 149 | ```r 150 | # ID token with AAD v1.0 151 | # if you only want an ID token, set the resource to blank ("") 152 | tok <- get_azure_token("", "mytenant", "app_id") 153 | extract_token(tok, "id") 154 | 155 | # ID token with AAD v2.0 156 | tok2 <- get_azure_token(c("openid", "offline_access"), "mytenant", "app_id", version=2) 157 | extract_token(tok2, "id") 158 | ``` 159 | 160 | ## Caching 161 | 162 | AzureAuth caches tokens based on all the inputs to `get_azure_token`, as listed above. It defines its own directory for cached tokens, using the rappdirs package. On recent Windows versions, this will usually be in the location `C:\Users\(username)\AppData\Local\AzureR`. On Linux, it will be in `~/.local/share/AzureR`, and on MacOS, it will be in `~/Library/Application Support/AzureR`. Note that a single directory is used for all tokens, and the working directory is not touched (which significantly lessens the risk of accidentally introducing cached tokens into source control). 163 | 164 | For reasons of CRAN policy, the first time that AzureAuth is loaded, it will prompt you for permission to create this directory. Unless you have a specific reason otherwise, it's recommended that you allow the directory to be created. Note that most other cloud engineering tools save credentials in this way, including Docker, Kubernetes, and the Azure CLI itself. The prompt only appears in an interactive session; if AzureAuth is loaded in a batch script, the directory is not created if it doesn't already exist. 165 | 166 | To list all cached tokens on disk, use `list_azure_tokens`. This returns a list of token objects, named according to their MD5 hashes. 167 | 168 | To load a token from the cache using its MD5 hash, use `load_azure_token`. To delete a cached token, use `delete_azure_token`. This takes the same inputs as `get_azure_token`, or you can supply an MD5 hash via the `hash` argument. To delete _all_ cached tokens, use `clean_token_directory`. 169 | 170 | ```r 171 | # list all tokens 172 | list_azure_tokens() 173 | 174 | # <... list of token objects ...> 175 | 176 | # delete a token 177 | delete_azure_token("myresource", "mytenant", "app_id", 178 | password="client_credentials", auth_type="client_credentials") 179 | ``` 180 | 181 | If you want to bypass the cache, specify `use_cache=FALSE` in the call to `get_azure_token`. This will always obtain a new token from AAD, and also prevent it being saved to the cache. 182 | 183 | ```r 184 | get_azure_token("myresource", "mytenant", "app_id", use_cache=FALSE) 185 | ``` 186 | 187 | ## Refreshing 188 | 189 | A token object can be refreshed by calling its `refresh()` method. If the token's credentials contain a refresh token, this is used; otherwise a new access token is obtained by reauthenticating. In most situations you don't need to worry about this, as the AzureR packages will check if the credentials have expired and automatically refresh them for you. 190 | 191 | One scenario where you might want to refresh manually is using a token for one resource to obtain a token for another resource. Note that in AAD, a refresh token can be used to obtain an access token for any resource or scope that you have permissions for. Thus, for example, you could use a refresh token issued on a request for `https://management.azure.com` to obtain a new access token for `https://graph.microsoft.com` (assuming you've been granted permission). 192 | 193 | To obtain an access token for a new resource, change the object's `resource` (for an AAD v1.0 token) or `scope` field (for an AAD v2.0 token) before calling `refresh()`. If you _also_ want to retain the token for the old resource, you should call the `clone()` method first to create a copy. 194 | 195 | ```r 196 | # use a refresh token from one resource to get an access token for another resource 197 | tok <- get_azure_token("https://myresource", "mytenant", "app_id") 198 | tok2 <- tok$clone() 199 | tok2$resource <- "https://anotherresource" 200 | tok2$refresh() 201 | 202 | # same for AAD v2.0 203 | tok <- get_azure_token(c("https://myresource/.default", "offline_access"), 204 | "mytenant", "app_id", version=2) 205 | tok2 <- tok$clone() 206 | tok2$scope <- c("https://anotherresource/.default", "offline_access") 207 | tok2$refresh() 208 | ``` 209 | 210 | ## More information 211 | 212 | For the details on Azure Active Directory, consult the [Microsoft documentation](https://docs.microsoft.com/en-us/azure/active-directory/develop/). 213 | 214 | 215 | --------------------------------------------------------------------------------