├── .github ├── workflows │ ├── LICENSE.md │ ├── update.yml │ ├── archive.yml │ ├── ghpages.yml │ └── publish.yml └── CODEOWNERS ├── LICENSE.md ├── .editorconfig ├── .gitignore ├── Makefile ├── README.md ├── CONTRIBUTING.md └── draft-parecki-oauth-dpop-device-flow.md /.github/workflows/LICENSE.md: -------------------------------------------------------------------------------- 1 | This project is in the public domain. 2 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # License 2 | 3 | See the 4 | [guidelines for contributions](https://github.com/aaronpk/oauth-dpop-device-flow/blob/main/CONTRIBUTING.md). 5 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # See http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*.{md,xml,org}] 6 | charset = utf-8 7 | insert_final_newline = true 8 | trim_trailing_whitespace = true 9 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Automatically generated CODEOWNERS 2 | # Regenerate with `make update-codeowners` 3 | draft-parecki-oauth-dpop-device-authorization-grant.md aaron.parecki@okta.com 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.html 2 | *.pdf 3 | *.redxml 4 | *.swp 5 | *.txt 6 | *.upload 7 | *~ 8 | .tags 9 | /*-[0-9][0-9].xml 10 | /.*.mk 11 | /.gems/ 12 | /.idea/ 13 | /.refcache 14 | /.venv/ 15 | /.vscode/ 16 | /lib 17 | /node_modules/ 18 | /versioned/ 19 | Gemfile.lock 20 | archive.json 21 | draft-parecki-oauth-dpop-device-flow.xml 22 | package-lock.json 23 | report.xml 24 | !requirements.txt 25 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | LIBDIR := lib 2 | include $(LIBDIR)/main.mk 3 | 4 | $(LIBDIR)/main.mk: 5 | ifneq (,$(shell grep "path *= *$(LIBDIR)" .gitmodules 2>/dev/null)) 6 | git submodule sync 7 | git submodule update --init 8 | else 9 | ifneq (,$(wildcard $(ID_TEMPLATE_HOME))) 10 | ln -s "$(ID_TEMPLATE_HOME)" $(LIBDIR) 11 | else 12 | git clone -q --depth 10 -b main \ 13 | https://github.com/martinthomson/i-d-template $(LIBDIR) 14 | endif 15 | endif 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # DPoP for the OAuth 2.0 Device Authorization Grant 4 | 5 | This is the working area for the individual Internet-Draft, "DPoP for the OAuth 2.0 Device Authorization Grant". 6 | 7 | * [Editor's Copy](https://drafts.aaronpk.com/oauth-dpop-device-flow/draft-parecki-oauth-dpop-device-flow.html) 8 | * [Datatracker Page](https://datatracker.ietf.org/doc/draft-parecki-oauth-dpop-device-flow) 9 | * [Individual Draft](https://datatracker.ietf.org/doc/html/draft-parecki-oauth-dpop-device-flow) 10 | 11 | 12 | ## Contributing 13 | 14 | See the 15 | [guidelines for contributions](https://github.com/aaronpk/oauth-dpop-device-flow/blob/main/CONTRIBUTING.md). 16 | 17 | The contributing file also has tips on how to make contributions, if you 18 | don't already know how to do that. 19 | 20 | -------------------------------------------------------------------------------- /.github/workflows/update.yml: -------------------------------------------------------------------------------- 1 | name: "Update Generated Files" 2 | # This rule is not run automatically. 3 | # It can be run manually to update all of the files that are part 4 | # of the template, specifically: 5 | # - README.md 6 | # - CONTRIBUTING.md 7 | # - .note.xml 8 | # - .github/CODEOWNERS 9 | # - Makefile 10 | # 11 | # 12 | # This might be useful if you have: 13 | # - added, removed, or renamed drafts (including after adoption) 14 | # - added, removed, or changed draft editors 15 | # - changed the title of drafts 16 | # 17 | # Note that this removes any customizations you have made to 18 | # the affected files. 19 | on: workflow_dispatch 20 | 21 | jobs: 22 | build: 23 | name: "Update Files" 24 | runs-on: ubuntu-latest 25 | permissions: 26 | contents: write 27 | steps: 28 | - name: "Checkout" 29 | uses: actions/checkout@v5 30 | 31 | - name: "Update Generated Files" 32 | uses: martinthomson/i-d-template@v1 33 | with: 34 | make: update-files 35 | token: ${{ github.token }} 36 | 37 | - name: "Push Update" 38 | run: git push 39 | -------------------------------------------------------------------------------- /.github/workflows/archive.yml: -------------------------------------------------------------------------------- 1 | name: "Archive Issues and Pull Requests" 2 | 3 | on: 4 | schedule: 5 | - cron: '0 0 * * 0,2,4' 6 | repository_dispatch: 7 | types: [archive] 8 | workflow_dispatch: 9 | inputs: 10 | archive_full: 11 | description: 'Recreate the archive from scratch' 12 | default: false 13 | type: boolean 14 | 15 | jobs: 16 | build: 17 | name: "Archive Issues and Pull Requests" 18 | runs-on: ubuntu-latest 19 | permissions: 20 | contents: write 21 | steps: 22 | - name: "Checkout" 23 | uses: actions/checkout@v5 24 | 25 | # Note: No caching for this build! 26 | 27 | - name: "Update Archive" 28 | uses: martinthomson/i-d-template@v1 29 | env: 30 | ARCHIVE_FULL: ${{ inputs.archive_full }} 31 | with: 32 | make: archive 33 | token: ${{ github.token }} 34 | 35 | - name: "Update GitHub Pages" 36 | uses: martinthomson/i-d-template@v1 37 | with: 38 | make: gh-archive 39 | token: ${{ github.token }} 40 | 41 | - name: "Save Archive" 42 | uses: actions/upload-artifact@v4 43 | with: 44 | name: archive 45 | path: archive.json 46 | -------------------------------------------------------------------------------- /.github/workflows/ghpages.yml: -------------------------------------------------------------------------------- 1 | name: "Update Editor's Copy" 2 | 3 | on: 4 | push: 5 | paths-ignore: 6 | - README.md 7 | - CONTRIBUTING.md 8 | - LICENSE.md 9 | - .gitignore 10 | pull_request: 11 | paths-ignore: 12 | - README.md 13 | - CONTRIBUTING.md 14 | - LICENSE.md 15 | - .gitignore 16 | 17 | jobs: 18 | build: 19 | name: "Update Editor's Copy" 20 | runs-on: ubuntu-latest 21 | permissions: 22 | contents: write 23 | steps: 24 | - name: "Checkout" 25 | uses: actions/checkout@v5 26 | 27 | - name: "Setup" 28 | id: setup 29 | run: date -u "+date=%FT%T" >>"$GITHUB_OUTPUT" 30 | 31 | - name: "Caching" 32 | uses: actions/cache@v4 33 | with: 34 | path: | 35 | .refcache 36 | .venv 37 | .gems 38 | node_modules 39 | .targets.mk 40 | key: i-d-${{ steps.setup.outputs.date }} 41 | restore-keys: i-d- 42 | 43 | - name: "Build Drafts" 44 | uses: martinthomson/i-d-template@v1 45 | with: 46 | token: ${{ github.token }} 47 | 48 | - name: "Update GitHub Pages" 49 | uses: martinthomson/i-d-template@v1 50 | if: ${{ github.event_name == 'push' }} 51 | with: 52 | make: gh-pages 53 | token: ${{ github.token }} 54 | 55 | - name: "Archive Built Drafts" 56 | uses: actions/upload-artifact@v4 57 | with: 58 | name: drafts 59 | path: | 60 | draft-*.html 61 | draft-*.txt 62 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: "Publish New Draft Version" 2 | 3 | on: 4 | push: 5 | tags: 6 | - "draft-*" 7 | workflow_dispatch: 8 | inputs: 9 | email: 10 | description: "Submitter email" 11 | default: "" 12 | type: string 13 | 14 | jobs: 15 | build: 16 | name: "Publish New Draft Version" 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: "Checkout" 20 | uses: actions/checkout@v5 21 | with: 22 | # Needed for tracing file history for replacement status 23 | fetch-depth: 0 24 | fetch-tags: true 25 | 26 | # See https://github.com/actions/checkout/issues/290 27 | - name: "Get Tag Annotations" 28 | run: git fetch -f origin ${{ github.ref }}:${{ github.ref }} 29 | 30 | - name: "Setup" 31 | id: setup 32 | run: date -u "+date=%FT%T" >>"$GITHUB_OUTPUT" 33 | 34 | - name: "Caching" 35 | uses: actions/cache@v4 36 | with: 37 | path: | 38 | .refcache 39 | .venv 40 | .gems 41 | node_modules 42 | .targets.mk 43 | key: i-d-${{ steps.setup.outputs.date }} 44 | restore-keys: i-d- 45 | 46 | - name: "Build Drafts" 47 | uses: martinthomson/i-d-template@v1 48 | with: 49 | token: ${{ github.token }} 50 | 51 | - name: "Upload to Datatracker" 52 | uses: martinthomson/i-d-template@v1 53 | with: 54 | make: upload 55 | env: 56 | UPLOAD_EMAIL: ${{ inputs.email }} 57 | 58 | - name: "Archive Submitted Drafts" 59 | uses: actions/upload-artifact@v4 60 | with: 61 | name: published 62 | path: "versioned/draft-*-[0-9][0-9].*" 63 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | This repository relates to activities in the Internet Engineering Task Force 4 | ([IETF](https://www.ietf.org/)). All material in this repository is considered 5 | Contributions to the IETF Standards Process, as defined in the intellectual 6 | property policies of IETF currently designated as 7 | [BCP 78](https://www.rfc-editor.org/info/bcp78), 8 | [BCP 79](https://www.rfc-editor.org/info/bcp79) and the 9 | [IETF Trust Legal Provisions (TLP) Relating to IETF Documents](http://trustee.ietf.org/trust-legal-provisions.html). 10 | 11 | Any edit, commit, pull request, issue, comment or other change made to this 12 | repository constitutes Contributions to the IETF Standards Process 13 | (https://www.ietf.org/). 14 | 15 | You agree to comply with all applicable IETF policies and procedures, including, 16 | BCP 78, 79, the TLP, and the TLP rules regarding code components (e.g. being 17 | subject to a Simplified BSD License) in Contributions. 18 | 19 | 20 | ## Working Group Information 21 | 22 | Discussion of this work occurs on the [Web Authorization Protocol 23 | Working Group mailing list](mailto:oauth@ietf.org) 24 | ([archive](https://mailarchive.ietf.org/arch/browse/oauth/), 25 | [subscribe](https://www.ietf.org/mailman/listinfo/oauth)). 26 | In addition to contributions in GitHub, you are encouraged to participate in 27 | discussions there. 28 | 29 | **Note**: Some working groups adopt a policy whereby substantive discussion of 30 | technical issues needs to occur on the mailing list. 31 | 32 | You might also like to familiarize yourself with other 33 | [Working Group documents](https://datatracker.ietf.org/wg/oauth/documents/). 34 | 35 | ## How to Contribute 36 | 37 | Contributions can be made by creating pull requests, opening an issue, or 38 | posting to the working group mailing list. See above for the email address 39 | and a note about policy. 40 | 41 | Here are two ways to create a pull request ("PR"): 42 | 43 | - Copy the repository and make a pull request using the Git command-line 44 | tool, using the [GitHub documentation](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request) if needed. 45 | 46 | - You can use the GitHub UI as follows: 47 | - View the draft source 48 | - Select the pencil icon to edit the file (usually top-right on the screen) 49 | - Make edits 50 | - Select "Commit changes" 51 | - Add a title and explanatory text 52 | - Select "Propose" 53 | - When prompted, click on "Create Pull Request" 54 | 55 | Document authors/editors are often happy to accept contributions of text, 56 | and might be willing to help you through the process. Email them and ask. 57 | -------------------------------------------------------------------------------- /draft-parecki-oauth-dpop-device-flow.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "DPoP for the OAuth 2.0 Device Authorization Grant" 3 | abbrev: "DPoP for Device Authorization Grant" 4 | category: std 5 | 6 | docname: draft-parecki-oauth-dpop-device-flow-latest 7 | submissiontype: IETF 8 | number: 9 | date: 10 | consensus: true 11 | v: 3 12 | area: "Security" 13 | workgroup: "Web Authorization Protocol" 14 | keyword: 15 | - oauth 16 | - device 17 | - dpop 18 | venue: 19 | group: "Web Authorization Protocol" 20 | type: "Working Group" 21 | mail: "oauth@ietf.org" 22 | arch: "https://mailarchive.ietf.org/arch/browse/oauth/" 23 | github: "aaronpk/oauth-dpop-device-flow" 24 | latest: "https://drafts.aaronpk.com/oauth-dpop-device-flow/draft-parecki-oauth-dpop-device-flow.html" 25 | 26 | author: 27 | - 28 | fullname: Aaron Parecki 29 | organization: Okta 30 | email: aaron@parecki.com 31 | - 32 | fullname: Brian Campbell 33 | organization: Ping Identity 34 | email: bcampbell@pingidentity.com 35 | 36 | normative: 37 | RFC6749: 38 | RFC8628: 39 | RFC9449: 40 | RFC9700: 41 | 42 | informative: 43 | RFC9126: 44 | 45 | ... 46 | 47 | --- abstract 48 | 49 | The OAuth 2.0 Device Authorization Grant {{RFC8628}} is an 50 | authorization flow for devices with limited input capabilities. 51 | Demonstrating Proof of Possession (DPoP) {{RFC9449}} is a mechanism 52 | to sender-constrain OAuth 2.0 tokens. This document describes how to 53 | use DPoP with the Device Authorization Grant to provide a higher 54 | level of security for clients. It binds the DPoP key to the entire transaction, 55 | from the initial device authorization request through the lifetime of 56 | the issued tokens. 57 | 58 | 59 | --- middle 60 | 61 | # Introduction 62 | 63 | The OAuth 2.0 Device Authorization Grant {{RFC8628}} provides a mechanism 64 | for devices that lack a browser or have constrained input capabilities 65 | to obtain an OAuth {{RFC6749}} access token. The flow involves the device polling the token 66 | endpoint while the user authorizes the request on a separate, more 67 | capable device. 68 | 69 | OAuth 2.0 Demonstrating Proof of Possession (DPoP) {{RFC9449}} 70 | introduces a mechanism for sender-constraining access and refresh 71 | tokens. It works by requiring the client to prove possession of a 72 | cryptographic key with every request, binding the tokens to that key. 73 | {{RFC9449}} explicitly details its application with Pushed Authorization 74 | Requests (PAR) {{RFC9126}}, a flow that, like the Device Authorization 75 | Grant, begins with a direct back-channel request from the client to the 76 | authorization server. 77 | 78 | This specification formally defines the mechanism of using DPoP with the Device 79 | Authorization Grant. By requiring a DPoP proof at the beginning of the 80 | flow, the authorization server can bind the `device_code` to a specific 81 | public key. This ensures that only the client possessing the 82 | corresponding private key can complete the flow by polling the token 83 | endpoint, thereby preventing a stolen `device_code` from being redeemed 84 | by a malicious actor. The resulting access and refresh tokens are also 85 | DPoP-bound, mitigating the risk of token leakage. 86 | 87 | # Conventions and Definitions 88 | 89 | {::boilerplate bcp14-tagged} 90 | 91 | 92 | # Protocol Flow 93 | 94 | The overall Device Authorization Grant flow remains as defined in 95 | {{RFC8628}}, with the addition of the DPoP header and associated 96 | validation logic at two key steps: the device authorization request and 97 | the device access token (polling) request. 98 | 99 | ## Device Authorization Request 100 | 101 | To initiate the flow, the client makes a POST request to the device 102 | authorization endpoint as specified in Section 3.1 of {{RFC8628}}. When 103 | using DPoP, this request MUST include a DPoP header field containing a 104 | valid DPoP proof JWT as defined in Section 4 of {{RFC9449}}. 105 | 106 | The authorization server MUST validate the DPoP proof according to the 107 | rules in Section 4.3 of {{RFC9449}}. If the DPoP proof is invalid, the 108 | authorization server MUST return an error response, and it is 109 | RECOMMENDED that this be a 400 Bad Request with an error code of 110 | `invalid_dpop_proof`. 111 | 112 | If the DPoP proof is valid, the authorization server proceeds as defined 113 | in Section 3.2 of {{RFC8628}} by generating a `device_code`, `user_code`, 114 | etc. In addition, the authorization server MUST associate the public key 115 | from the `jwk` header of the DPoP proof with the generated `device_code` 116 | and store this association for later verification. 117 | 118 | For example: 119 | 120 | POST /device_authorization HTTP/1.1 121 | Host: server.example.com 122 | Content-Type: application/x-www-form-urlencoded 123 | DPoP: eyJ0eXAiOiJkcG9wK2p3dCIsImFsZyI6IkVTMjU2IiwiandrIjp7Imt0eSI6Ik... 124 | 125 | client_id=1406020730&scope=example_scope 126 | 127 | ## Device Access Token Request 128 | 129 | After the user authorizes the request, the client begins polling the 130 | token endpoint as described in Section 3.4 of {{RFC8628}}. Each access 131 | token request (polling request) using the 132 | `urn:ietf:params:oauth:grant-type:device_code` grant type MUST include a 133 | DPoP header with DPoP proof JWT. 134 | 135 | Upon receiving a token request, the authorization server MUST perform 136 | the following steps in addition to the processing described in {{RFC8628}}: 137 | 138 | * Look up the data associated with the received `device_code`. 139 | * Retrieve the public key that was associated with the `device_code` during the device authorization request. 140 | * Validate the DPoP proof JWT in the DPoP header of the current polling request per Section 4.3 of {{RFC9449}}. 141 | * Verify that the public key in the `jwk` header of the DPoP proof matches the public key associated with the `device_code`. 142 | 143 | If any of these checks fail, the authorization server MUST reject 144 | the request with an `invalid_grant` error. 145 | 146 | If all checks are successful and the user has approved the grant, the 147 | authorization server issues an access token and an optional refresh 148 | token. The issued access token MUST be bound to the DPoP public key, 149 | and the access token response MUST include `"token_type": "DPoP"``, as 150 | specified in Section 5 of {{RFC9449}}. 151 | 152 | Any issued refresh token MUST also be bound to the same DPoP public key. 153 | 154 | 155 | # Security Considerations 156 | 157 | ## Device Code Binding 158 | 159 | The primary security benefit of this specification is the binding of 160 | the DPoP key to the `device_code` at the start of the authorization 161 | flow. In the standard Device Authorization Grant, an attacker who obtains 162 | a valid `device_code` (e.g., through log inspection or a compromised 163 | device) can start polling the token endpoint. If the attacker completes 164 | the user authorization step (e.g., via a phishing attack that tricks the 165 | user into entering the `user_code`), they can obtain the access token. 166 | 167 | By binding the `device_code` to the client's DPoP key, this attack is 168 | prevented. The attacker's polling requests to the token endpoint will 169 | fail because they cannot produce a valid DPoP proof signed with the 170 | private key corresponding to the public key bound to the `device_code`. 171 | Only the legitimate client can successfully redeem the `device_code`. 172 | 173 | ## Client Impersonation 174 | 175 | This specification does not prevent a malicious application on a device 176 | from initiating a device flow and using DPoP correctly. It protects the 177 | integrity of a single authorization flow by ensuring the same cryptographic 178 | identity (the DPoP key pair) is used throughout. It is not a client 179 | authentication mechanism. As such, the security considerations for public 180 | clients in Section 5 of {{RFC8628}} and {{RFC9700}} remain relevant. 181 | 182 | 183 | ## General DPoP Considerations 184 | 185 | All security considerations from {{RFC9449}} apply, including those 186 | regarding DPoP proof replay, nonce usage, signature algorithms, and the 187 | need to protect the private key on the client device. 188 | 189 | 190 | # IANA Considerations 191 | 192 | This document has no IANA actions. 193 | 194 | 195 | --- back 196 | 197 | # Acknowledgments 198 | {:numbered="false"} 199 | 200 | The authors would like to thank Emelia Smith for her reply on 201 | social media suggesting that this document should exist. 202 | 203 | 204 | # Document History 205 | {:numbered="false"} 206 | 207 | \[\[ To be removed from the final specification ]] 208 | 209 | -00 210 | 211 | * Initial draft 212 | --------------------------------------------------------------------------------