├── .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 | [](https://cran.r-project.org/package=AzureAuth)
4 | 
5 | 
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 | 
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 | 
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 | 
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 |
--------------------------------------------------------------------------------