├── .gitignore ├── src ├── compat │ ├── mod.rs │ └── daemon.rs ├── config_ast.rs ├── config.l ├── server │ ├── request_token.rs │ ├── eventer.rs │ ├── notifier.rs │ ├── mod.rs │ ├── http_server.rs │ ├── refresher.rs │ └── state.rs ├── config.y ├── user_sender.rs └── main.rs ├── examples ├── systemd │ └── pizauth.conf ├── pizauth.conf └── pizauth-state-custom.service ├── LICENSE-APACHE ├── COPYRIGHT ├── lib └── systemd │ └── user │ ├── pizauth-state-creds.service │ ├── pizauth.service │ ├── pizauth-state-gpg.service │ ├── pizauth-state-age.service │ └── pizauth-state-gpg-passphrase.service ├── LICENSE-MIT ├── .github └── workflows │ └── ci.yml ├── Cargo.toml ├── share ├── bash │ └── completion.bash └── fish │ └── pizauth.fish ├── Makefile ├── README.systemd.md ├── pizauth.1 ├── pizauth.conf.5 ├── CHANGES.md └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | -------------------------------------------------------------------------------- /src/compat/mod.rs: -------------------------------------------------------------------------------- 1 | //! Shims to provide compatibility with different systems. 2 | 3 | // nix does not support daemon(3) on macOS, so we have to provide our own implementation: 4 | #[cfg(target_os = "macos")] 5 | mod daemon; 6 | #[cfg(target_os = "macos")] 7 | pub use daemon::daemon; 8 | 9 | // Use nix's daemon(3) wrapper on other platforms: 10 | #[cfg(not(target_os = "macos"))] 11 | pub use nix::unistd::daemon; 12 | -------------------------------------------------------------------------------- /examples/systemd/pizauth.conf: -------------------------------------------------------------------------------- 1 | // If using systemd, comment out the following line 2 | // startup_cmd="systemd-notify --ready --pid=parent"; 3 | // If using the pizauth-state-*.service units to save the state, 4 | // the following may be useful -- it will trigger a save/restore of the state 5 | // upon each token state change (set METHOD to whatever storage method you're 6 | // using) 7 | // token_event_cmd="systemctl --user restart pizauth-state-METHOD.service"; 8 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use 2 | this file except in compliance with the License. You may obtain a copy of the 3 | License at 4 | 5 | http://www.apache.org/licenses/LICENSE-2.0 6 | 7 | Unless required by applicable law or agreed to in writing, software distributed 8 | under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 9 | CONDITIONS OF ANY KIND, either express or implied. See the License for the 10 | specific language governing permissions and limitations under the License. 11 | -------------------------------------------------------------------------------- /COPYRIGHT: -------------------------------------------------------------------------------- 1 | Except as otherwise noted (below and/or in individual files), this project is 2 | licensed under the Apache License, Version 2.0 3 | or the MIT license 4 | , at your option. 5 | 6 | Copyright is retained by contributors and/or the organisations they 7 | represent(ed) -- this project does not require copyright assignment. Please see 8 | version control history for a full list of contributors. Note that some files 9 | may include explicit copyright and/or licensing notices. 10 | -------------------------------------------------------------------------------- /lib/systemd/user/pizauth-state-creds.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=pizauth dump/restore backend (encryption: systemd-creds) 3 | BindsTo=pizauth.service 4 | After=pizauth.service 5 | ReloadPropagatedFrom=pizauth.service 6 | 7 | [Service] 8 | Type=simple 9 | RemainAfterExit=yes 10 | Environment="PIZAUTH_STATE_FILE=%S/%N.dump" 11 | ExecStart=-sh -c 'systemd-creds --user decrypt $PIZAUTH_STATE_FILE | pizauth restore' 12 | ExecReload=-sh -c 'systemd-creds --user decrypt $PIZAUTH_STATE_FILE | pizauth restore' 13 | ExecStop=-sh -c 'pizauth dump | systemd-creds --user encrypt - $PIZAUTH_STATE_FILE' 14 | 15 | [Install] 16 | WantedBy=pizauth.service 17 | -------------------------------------------------------------------------------- /lib/systemd/user/pizauth.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Pizauth OAuth2 token manager 3 | Documentation=man:pizauth(1) man:pizauth.conf(5) 4 | Documentation=https://github.com/ltratt/pizauth/blob/master/README.md 5 | Documentation=https://github.com/ltratt/pizauth/blob/master/README.systemd.md 6 | 7 | [Service] 8 | Type=notify 9 | # Allow all processes in the cgroup to set the service status, needed to be able 10 | # to set status via eg startup_cmd, token_event_cmd, etc 11 | NotifyAccess=all 12 | ExecStart=/usr/bin/pizauth server -vvvv -d 13 | ExecReload=/usr/bin/pizauth reload 14 | ExecStop=/usr/bin/pizauth shutdown 15 | 16 | [Install] 17 | WantedBy=default.target 18 | -------------------------------------------------------------------------------- /examples/pizauth.conf: -------------------------------------------------------------------------------- 1 | account "officesmtp" { 2 | auth_uri = "https://login.microsoftonline.com/common/oauth2/v2.0/authorize"; 3 | token_uri = "https://login.microsoftonline.com/common/oauth2/v2.0/token"; 4 | client_id = "..."; // Fill in with your Client ID 5 | client_secret = "..."; // Fill in with your Client secret 6 | scopes = [ 7 | "https://outlook.office365.com/IMAP.AccessAsUser.All", 8 | "https://outlook.office365.com/SMTP.Send", 9 | "offline_access" 10 | ]; 11 | // You don't have to specify login_hint, but it does make 12 | // authentication a little easier. 13 | auth_uri_fields = { "login_hint": "email@example.com" }; 14 | } 15 | -------------------------------------------------------------------------------- /src/config_ast.rs: -------------------------------------------------------------------------------- 1 | use lrpar::Span; 2 | 3 | pub enum TopLevel { 4 | Account(Span, Span, Vec), 5 | AuthErrorCmd(Span), 6 | AuthNotifyCmd(Span), 7 | AuthNotifyInterval(Span), 8 | ErrorNotifyCmd(Span), 9 | HttpListen(Span), 10 | HttpListenNone(Span), 11 | HttpsListen(Span), 12 | HttpsListenNone(Span), 13 | TransientErrorIfCmd(Span), 14 | RefreshAtLeast(Span), 15 | RefreshBeforeExpiry(Span), 16 | RefreshRetry(Span), 17 | StartupCmd(Span), 18 | TokenEventCmd(Span), 19 | } 20 | 21 | pub enum AccountField { 22 | AuthUri(Span), 23 | AuthUriFields(Span, Vec<(Span, Span)>), 24 | ClientId(Span), 25 | ClientSecret(Span), 26 | LoginHint(Span), 27 | RedirectUri(Span), 28 | RefreshAtLeast(Span), 29 | RefreshBeforeExpiry(Span), 30 | RefreshRetry(Span), 31 | Scopes(Span, Vec), 32 | TokenUri(Span), 33 | } 34 | -------------------------------------------------------------------------------- /src/config.l: -------------------------------------------------------------------------------- 1 | %% 2 | [0-9]+[dhms] "TIME" 3 | "(?:\\[\\"]|[^"\\])*" "STRING" 4 | = "=" 5 | , "," 6 | \{ "{" 7 | \} "}" 8 | \[ "[" 9 | \] "]" 10 | ; ";" 11 | : ":" 12 | account "ACCOUNT" 13 | auth_error_cmd "AUTH_ERROR_CMD" 14 | auth_notify_cmd "AUTH_NOTIFY_CMD" 15 | auth_notify_interval "AUTH_NOTIFY_INTERVAL" 16 | auth_uri "AUTH_URI" 17 | auth_uri_fields "AUTH_URI_FIELDS" 18 | client_id "CLIENT_ID" 19 | client_secret "CLIENT_SECRET" 20 | error_notify_cmd "ERROR_NOTIFY_CMD" 21 | http_listen "HTTP_LISTEN" 22 | https_listen "HTTPS_LISTEN" 23 | login_hint "LOGIN_HINT" 24 | none "NONE" 25 | refresh_retry "REFRESH_RETRY" 26 | redirect_uri "REDIRECT_URI" 27 | refresh_before_expiry "REFRESH_BEFORE_EXPIRY" 28 | refresh_at_least "REFRESH_AT_LEAST" 29 | scopes "SCOPES" 30 | startup_cmd "STARTUP_CMD" 31 | token_event_cmd "TOKEN_EVENT_CMD" 32 | token_uri "TOKEN_URI" 33 | transient_error_if_cmd "TRANSIENT_ERROR_IF_CMD" 34 | //.*?$ ; 35 | [ \t\n\r]+ ; 36 | . "UNMATCHED" 37 | -------------------------------------------------------------------------------- /lib/systemd/user/pizauth-state-gpg.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=pizauth dump/restore backend (encryption: gpg) 3 | BindsTo=pizauth.service 4 | After=pizauth.service 5 | ReloadPropagatedFrom=pizauth.service 6 | Requires=gpg-agent.socket 7 | 8 | [Service] 9 | Type=simple 10 | RemainAfterExit=yes 11 | # This unit needs to be configured before it's usable! 12 | # To use this unit, use systemctl --user edit pizauth-state-age.service to 13 | # create a drop-in configuration file. In it, set 14 | # Environment="PIZAUTH_KEY_ID=public key you want to encrypt with" 15 | Environment="PIZAUTH_KEY_ID=" 16 | Environment="PIZAUTH_STATE_FILE=%S/%N.dump" 17 | ExecStart=-sh -c 'gpg --batch --decrypt "$PIZAUTH_STATE_FILE" | pizauth restore' 18 | ExecReload=-sh -c 'gpg --batch --decrypt "$PIZAUTH_STATE_FILE" | pizauth restore' 19 | ExecStop=-sh -c 'pizauth dump | gpg --batch --yes --encrypt --recipient $PIZAUTH_KEY_ID -o "$PIZAUTH_STATE_FILE"' 20 | 21 | [Install] 22 | WantedBy=pizauth.service 23 | -------------------------------------------------------------------------------- /lib/systemd/user/pizauth-state-age.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=pizauth dump/restore backend (encryption: age) 3 | BindsTo=pizauth.service 4 | After=pizauth.service 5 | ReloadPropagatedFrom=pizauth.service 6 | 7 | [Service] 8 | Type=simple 9 | RemainAfterExit=yes 10 | # This unit needs to be configured before it's usable! 11 | # To use this unit, use systemctl --user edit pizauth-state-age.service to 12 | # create a drop-in configuration file. In it, set 13 | # Environment="PIZAUTH_KEY_ID=public key you want to encrypt with" 14 | # Environment="PIZAUTH_KEY_FILE=path to file 15 | Environment="PIZAUTH_KEY_ID=" 16 | Environment="PIZAUTH_KEY_FILE=" 17 | Environment="PIZAUTH_STATE_FILE=%S/%N.dump" 18 | ExecStart=-sh -c 'age --decrypt --identity "$PIZAUTH_KEY_FILE" -o - "$PIZAUTH_STATE_FILE" | pizauth restore' 19 | ExecReload=-sh -c 'age --decrypt --identity "$PIZAUTH_KEY_FILE" -o - "$PIZAUTH_STATE_FILE" | pizauth restore' 20 | ExecStop=-sh -c 'pizauth dump | age --encrypt --recipient "$PIZAUTH_KEY_ID" -o "$PIZAUTH_STATE_FILE"' 21 | 22 | [Install] 23 | WantedBy=pizauth.service 24 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any person obtaining a copy of 2 | this software and associated documentation files (the "Software"), to deal in 3 | the Software without restriction, including without limitation the rights to 4 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 5 | of the Software, and to permit persons to whom the Software is furnished to do 6 | so, subject to the following conditions: 7 | 8 | The above copyright notice and this permission notice shall be included in all 9 | copies or substantial portions of the Software. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 12 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 13 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 14 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 15 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 16 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 17 | SOFTWARE. 18 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | 6 | jobs: 7 | rustfmt: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v3 11 | - run: rustup update stable && rustup default stable 12 | - run: rustup component add rustfmt 13 | - run: cargo fmt --all --check 14 | 15 | test: 16 | runs-on: ${{ matrix.os }} 17 | strategy: 18 | matrix: 19 | include: 20 | - name: Linux x86_64 stable 21 | os: ubuntu-latest 22 | rust: stable 23 | other: i686-unknown-linux-gnu 24 | - name: Linux x86_64 beta 25 | os: ubuntu-latest 26 | rust: beta 27 | other: i686-unknown-linux-gnu 28 | - name: Linux x86_64 nightly 29 | os: ubuntu-latest 30 | rust: nightly 31 | other: i686-unknown-linux-gnu 32 | - name: macOS x86_64 stable 33 | os: macos-latest 34 | rust: stable 35 | other: x86_64-apple-ios 36 | name: Tests ${{ matrix.name }} 37 | steps: 38 | - uses: actions/checkout@v3 39 | - run: rustup update stable && rustup default stable 40 | - name: debug_tests 41 | run: cargo test 42 | - name: release_tests 43 | run: cargo test --release 44 | -------------------------------------------------------------------------------- /src/compat/daemon.rs: -------------------------------------------------------------------------------- 1 | //! Provides daemon(3) on macOS. 2 | 3 | // We provide our own wrapper for daemon on macOS because nix does not export one for macOS. This 4 | // is *probably* why nix does not support daemon(3) on macOS: 5 | // 6 | // - nix will not compile on macOS, due to errors 7 | // - ... nix compiles with #[deny(warnings)], which treats warnings as errors 8 | // - libc emits a deprecation warning for daemon(3) on macOS [1] 9 | // - ... because daemon(3) has been deprecated in macOS since Mac OS X 10.5 10 | // - ... presumably because Apple wants you to use launchd(8) instead [2]. 11 | // - Therefore, this deprecation warning is treated as an error in nix 12 | // 13 | // [1]: https://github.com/rust-lang/libc/blob/96c85c1b913604fb5b1eb8822e344b7c08bcd6b9/src/unix/bsd/apple/mod.rs#L5064-L5067 14 | // [2]: https://developer.apple.com/library/archive/documentation/MacOSX/Conceptual/BPSystemStartup/Chapters/CreatingLaunchdJobs.html 15 | // 16 | // This module essentially reimplements nix's daemon wrapper on macOS, but allows deprecation 17 | // warnings. 18 | // 19 | // See: https://github.com/ltratt/pizauth/issues/3 20 | use libc::c_int; 21 | #[allow(deprecated)] 22 | use libc::daemon as libc_daemon; 23 | use nix::errno::Errno; 24 | 25 | pub fn daemon(nochdir: bool, noclose: bool) -> nix::Result<()> { 26 | #[allow(deprecated)] 27 | let res = unsafe { libc_daemon(nochdir as c_int, noclose as c_int) }; 28 | Errno::result(res).map(drop) 29 | } 30 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "pizauth" 3 | description = "Command-line OAuth2 authentication daemon" 4 | version = "1.0.8" 5 | repository = "https://github.com/ltratt/pizauth/" 6 | authors = ["Laurence Tratt "] 7 | readme = "README.md" 8 | license = "Apache-2.0 OR MIT" 9 | categories = ["authentication"] 10 | keywords = ["oauth", "oauth2", "authentication"] 11 | edition = "2021" 12 | 13 | [build-dependencies] 14 | cfgrammar = "0.14" 15 | lrlex = "0.14" 16 | lrpar = "0.14" 17 | rerun_except = "1" 18 | 19 | [dependencies] 20 | base64 = "0.22" 21 | bincode = { version="2", features=["serde"] } 22 | boot-time = "0.1.2" 23 | cfgrammar = "0.14" 24 | chacha20poly1305 = "0.10" 25 | chrono = "0.4" 26 | getopts = "0.2" 27 | hostname = "0.4" 28 | log = "0.4" 29 | lrlex = "0.14" 30 | lrpar = "0.14" 31 | nix = { version="0.29", features=["fs", "signal"] } 32 | rand = "0.9" 33 | serde = { version="1.0", features=["derive"] } 34 | sha2 = "0.10" 35 | serde_json = "1" 36 | stderrlog = "0.6" 37 | syslog = "7.0.0" 38 | ureq = "3" 39 | url = "2" 40 | wait-timeout = "0.2" 41 | whoami = "1.5" 42 | rustls = { version = "0.23.12", features = ["ring", "std"], default-features = false } 43 | rcgen = { version = "0.14.5", features = ["crypto", "ring"], default-features = false } 44 | 45 | [target.'cfg(target_os="openbsd")'.dependencies] 46 | pledge = "0.4" 47 | unveil = "0.3" 48 | 49 | [target.'cfg(target_os="macos")'.dependencies] 50 | libc = "0.2" 51 | 52 | [profile.release] 53 | opt-level = 3 54 | debug = false 55 | rpath = false 56 | lto = true 57 | debug-assertions = false 58 | codegen-units = 1 59 | panic = 'abort' 60 | incremental = false 61 | overflow-checks = true 62 | -------------------------------------------------------------------------------- /examples/pizauth-state-custom.service: -------------------------------------------------------------------------------- 1 | # In case the supplied pizauth-state-*.service files don't suit your needs, 2 | # this is the template for a new pizauth-state-*.service file. 3 | # See systemd.service(5), systemd.unit(5), systemd.exec(5) for more details. 4 | # 5 | # We pull out the dump/restore feature as its own unit since under the systemd 6 | # semantics, it makes more sense -- as indicated by the fact that were we to 7 | # try to configure this directly in the pizauth unit, we'd need to prepend to 8 | # ExecStop. Moreover, pizauth *works* without dump/restore -- this is an 9 | # additional feature on top of it. 10 | # 11 | # Hence, we have a separate dump/restore unit that gets started after pizauth 12 | # and torn down before it, and which is responsible for managing the state file 13 | # upon these events. 14 | 15 | [Unit] 16 | Description=Custom pizauth dump/restore backend 17 | # Makes the start event for this unit propagate to pizauth, 18 | # and makes the stop/abort events for pizauth propagate to this unit 19 | BindsTo=pizauth.service 20 | # Orders this unit to run its start commands after pizauth, and run its stop 21 | # commands before pizauth 22 | After=pizauth.service 23 | # Makes the config-reload event for pizauth propagate to this unit 24 | ReloadPropagatedFrom=pizauth.service 25 | 26 | [Service] 27 | Type=simple 28 | Environment="PIZAUTH_STATE_FILE=%S/%N.dump" 29 | # replace io by whatever program you have to read/write to the state file 30 | # (could be encryption software, could be sending/retrieving to a server, etc) 31 | ExecStart=-sh -c 'io --read "$PIZAUTH_STATE_FILE" | pizauth restore' 32 | ExecReload=-sh -c 'io --read "$PIZAUTH_STATE_FILE" | pizauth restore' 33 | ExecStop=-sh -c 'pizauth dump | io --write "$PIZAUTH_STATE_FILE"' 34 | 35 | [Install] 36 | # Makes systemctl --user enable pizauth-state-custom.service cause that the 37 | # start event for pizauth will propagate to this unit 38 | WantedBy=pizauth.service 39 | -------------------------------------------------------------------------------- /lib/systemd/user/pizauth-state-gpg-passphrase.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=pizauth dump/restore backend (encryption: gpg, passphrase in systemd-creds) 3 | BindsTo=pizauth.service 4 | After=pizauth.service 5 | ReloadPropagatedFrom=pizauth.service 6 | Requires=gpg-agent.socket 7 | 8 | [Service] 9 | # Since we're using credentials, we can't use Type=simple 10 | Type=exec 11 | RemainAfterExit=yes 12 | # This unit needs to be configured before it's usable! 13 | # To use this unit, use systemctl --user edit pizauth-state-gpg.service to 14 | # create a drop-in configuration file. In it, set 15 | # Environment="PIZAUTH_KEY_ID=public key you want to encrypt with" 16 | # and then either 17 | # LoadCredentialEncrypted=pizauth-gpg-passphrase:CREDENTIALFILE 18 | # or 19 | # SetCredentialEncrypted=pizauth-gpg-passphrase: \ 20 | # ..................................................................... \ 21 | # ... 22 | # 23 | # In either case, you will need to store the passphrase for the GPG key 24 | # encrypted. If you plan on storing the credential at CREDENTIALFILE, run 25 | # systemd-ask-password \ 26 | # | systemd-creds encrypt --name pizauth-gpg-passphrase - CREDENTIALFILE 27 | # If you want to store the credential in the drop-in configuration, run 28 | # systemd-ask-password \ 29 | # | systemd-creds encrypt --name pizauth-gpg-passphrase -p - - 30 | # This will print the SetCredentialEncrypted config you'll need to paste in the 31 | # drop-in configuration 32 | Environment="PIZAUTH_KEY_ID=" 33 | Environment="PIZAUTH_STATE_FILE=%S/%N.dump" 34 | ExecStart=-sh -c ' gpg --passphrase-file %d/pizauth-gpg-passphrase --pinentry-mode loopback --batch --decrypt "$PIZAUTH_STATE_FILE" \ 35 | | pizauth restore' 36 | ExecReload=-sh -c ' gpg --passphrase-file %d/pizauth-gpg-passphrase --pinentry-mode loopback --batch --decrypt "$PIZAUTH_STATE_FILE" \ 37 | | pizauth restore' 38 | ExecStop=-sh -c 'pizauth dump | gpg --batch --yes --encrypt --recipient $PIZAUTH_KEY_ID -o "$PIZAUTH_STATE_FILE"' 39 | 40 | [Install] 41 | WantedBy=pizauth.service 42 | -------------------------------------------------------------------------------- /src/server/request_token.rs: -------------------------------------------------------------------------------- 1 | use std::{error::Error, sync::Arc}; 2 | 3 | use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine}; 4 | use rand::{rng, RngCore}; 5 | use sha2::{Digest, Sha256}; 6 | use url::Url; 7 | 8 | use super::{AccountId, AuthenticatorState, CTGuard, TokenState, CODE_VERIFIER_LEN, STATE_LEN}; 9 | 10 | /// Request a new token for `act_id`, whose tokenstate must be `Empty`. 11 | pub fn request_token( 12 | pstate: Arc, 13 | mut ct_lk: CTGuard, 14 | act_id: AccountId, 15 | ) -> Result> { 16 | assert!(matches!( 17 | ct_lk.tokenstate(act_id), 18 | TokenState::Empty | TokenState::Pending { .. } 19 | )); 20 | 21 | let act = ct_lk.account(act_id); 22 | 23 | let mut state = [0u8; STATE_LEN]; 24 | rng().fill_bytes(&mut state); 25 | let state = URL_SAFE_NO_PAD.encode(state); 26 | 27 | let mut code_verifier = [0u8; CODE_VERIFIER_LEN]; 28 | rng().fill_bytes(&mut code_verifier); 29 | let code_verifier = URL_SAFE_NO_PAD.encode(code_verifier); 30 | let mut hasher = Sha256::new(); 31 | hasher.update(&code_verifier); 32 | let code_challenge = URL_SAFE_NO_PAD.encode(hasher.finalize()); 33 | 34 | let scopes_join = act.scopes.join(" "); 35 | let redirect_uri = act 36 | .redirect_uri(pstate.http_port, pstate.https_port)? 37 | .to_string(); 38 | let mut params = vec![ 39 | ("access_type", "offline"), 40 | ("code_challenge", &code_challenge), 41 | ("code_challenge_method", "S256"), 42 | ("client_id", act.client_id.as_str()), 43 | ("redirect_uri", redirect_uri.as_str()), 44 | ("response_type", "code"), 45 | ("state", &state), 46 | ]; 47 | if !act.scopes.is_empty() { 48 | params.push(("scope", scopes_join.as_str())); 49 | } 50 | for (k, v) in &act.auth_uri_fields { 51 | params.push((k.as_str(), v.as_str())); 52 | } 53 | let url = Url::parse_with_params(ct_lk.account(act_id).auth_uri.as_str(), ¶ms)?; 54 | ct_lk.tokenstate_replace( 55 | act_id, 56 | TokenState::Pending { 57 | code_verifier, 58 | last_notification: None, 59 | url: url.clone(), 60 | state, 61 | }, 62 | ); 63 | drop(ct_lk); 64 | pstate.notifier.notify_changes(); 65 | Ok(url) 66 | } 67 | -------------------------------------------------------------------------------- /share/bash/completion.bash: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | _server() { 3 | local cur prev 4 | 5 | prev=${COMP_WORDS[COMP_CWORD - 1]} 6 | cur=${COMP_WORDS[COMP_CWORD]} 7 | case "$prev" in 8 | -c) _filedir;; 9 | *) mapfile -t COMPREPLY < \ 10 | <(compgen -W '-c -d -v -vv -vvv -vvvv' -- "$cur");; 11 | esac 12 | } 13 | _accounts(){ 14 | local config 15 | 16 | config="$(pizauth info | awk -F' *: *' '$1 ~ /config file/ { print $2 }')" 17 | sed -n '/^account/{s/^account \(.*\) {/\1/;p}' "$config" 18 | } 19 | _pizauth() 20 | { 21 | local cur prev sub 22 | local cmds=() 23 | cmds+=(dump restore reload shutdown status) 24 | cmds+=(info server) 25 | cmds+=(refresh revoke show) 26 | 27 | cur=${COMP_WORDS[COMP_CWORD]} 28 | prev=${COMP_WORDS[COMP_CWORD - 1]} 29 | sub=${COMP_WORDS[1]} 30 | 31 | if [ "$sub" == server ] && [ "$COMP_CWORD" -gt 1 ]; then _server; return; fi 32 | 33 | case ${COMP_CWORD} in 34 | 1) mapfile -t COMPREPLY < <(compgen -W "${cmds[*]}" -- "$cur");; 35 | 2) 36 | case $sub in 37 | dump|restore|reload|shutdown|status) COMPREPLY=();; 38 | info) mapfile -t COMPREPLY < <(compgen -W '-j' -- "$cur") ;; 39 | refresh|show) 40 | local accounts 41 | mapfile -t accounts < <(_accounts) 42 | accounts+=(-u) 43 | mapfile -t COMPREPLY < \ 44 | <(compgen -W "${accounts[*]}" -- "$cur") 45 | ;; 46 | revoke) 47 | local accounts 48 | mapfile -t accounts < <(_accounts) 49 | mapfile -t COMPREPLY < \ 50 | <(compgen -W "${accounts[*]}" -- "$cur") 51 | ;; 52 | *) COMPREPLY=() 53 | ;; 54 | esac 55 | ;; 56 | 3) 57 | case $sub in 58 | refresh|show) 59 | case $prev in 60 | -u) 61 | local accounts 62 | mapfile -t accounts < <(_accounts) 63 | mapfile -t COMPREPLY < \ 64 | <(compgen -W "${accounts[*]}" -- "$cur") 65 | ;; 66 | *) COMPREPLY=() 67 | esac 68 | ;; 69 | esac 70 | ;; 71 | *) 72 | COMPREPLY=() 73 | ;; 74 | esac 75 | } 76 | 77 | complete -F _pizauth pizauth 78 | -------------------------------------------------------------------------------- /share/fish/pizauth.fish: -------------------------------------------------------------------------------- 1 | #!/usr/bin/fish 2 | 3 | function __fish_pizauth_accounts --description "Helper function to parse accounts from config" 4 | set -l config (pizauth info | awk -F' *: *' '$1 ~ /config file/ { print $2 }') 5 | sed -n '/^account/{s/^account \(.*\) {/\1/;p}' $config | string unescape 6 | end 7 | 8 | function __fish_pizauth_is_main_command --description "Returns true if we're not in a subcommand" 9 | not __fish_seen_subcommand_from dump restore reload shutdown status info server refresh revoke show 10 | end 11 | 12 | # Don't autocomplete files 13 | complete -c pizauth -f 14 | 15 | # pizauth top-level commands 16 | complete -c pizauth -n "__fish_pizauth_is_main_command" -d "Writes current pizauth state to stdout" -a "dump" 17 | complete -c pizauth -n "__fish_pizauth_is_main_command" -d "Writes output about pizauth to stdout" -a "info" 18 | complete -c pizauth -n "__fish_pizauth_is_main_command" -d "Request a refresh of the access token for account" -a "refresh" 19 | complete -c pizauth -n "__fish_pizauth_is_main_command" -d "Reloads the server's configuration" -a "reload" 20 | complete -c pizauth -n "__fish_pizauth_is_main_command" -d "Reads previously dumped pizauth state from stdin" -a "restore" 21 | complete -c pizauth -n "__fish_pizauth_is_main_command" -d "Removes token and cancels authorization for account" -a "revoke" 22 | complete -c pizauth -n "__fish_pizauth_is_main_command" -d "Start the server" -a "server" 23 | complete -c pizauth -n "__fish_pizauth_is_main_command" -d "Print access token of account to stdout" -a "show" 24 | complete -c pizauth -n "__fish_pizauth_is_main_command" -d "Shut the server down" -a "shutdown" 25 | complete -c pizauth -n "__fish_pizauth_is_main_command" -d "Writes output about current accounts to stdout" -a "status" 26 | 27 | # pizauth info [-j] 28 | complete -c pizauth -n "__fish_seen_subcommand_from info" -s j -d "JSON output" 29 | 30 | # pizauth refresh/show [-u] account 31 | complete -c pizauth -n "__fish_seen_subcommand_from refresh show" -s u -d "Exclude authorization URL" 32 | complete -c pizauth -n "__fish_seen_subcommand_from refresh show" -a "(__fish_pizauth_accounts)" 33 | 34 | # pizauth revoke account 35 | complete -c pizauth -n "__fish_seen_subcommand_from revoke" -a "(__fish_pizauth_accounts)" 36 | 37 | # pizauth server [-c config-file] [-dv] 38 | complete -c pizauth -n "__fish_seen_subcommand_from server" -l config -s c -r -F -d "Config file" 39 | complete -c pizauth -n "__fish_seen_subcommand_from server" -s d -d "Do not daemonise" 40 | complete -c pizauth -n "__fish_seen_subcommand_from server" -o v -d "Verbose" 41 | complete -c pizauth -n "__fish_seen_subcommand_from server" -o vv -d "Verboser" 42 | complete -c pizauth -n "__fish_seen_subcommand_from server" -o vvv -d "Verboserer" 43 | complete -c pizauth -n "__fish_seen_subcommand_from server" -o vvvv -d "Verbosest" 44 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PREFIX ?= /usr/local 2 | BINDIR ?= ${PREFIX}/bin 3 | LIBDIR ?= ${PREFIX}/lib 4 | SHAREDIR ?= ${PREFIX}/share 5 | EXAMPLESDIR ?= ${SHAREDIR}/examples 6 | 7 | MANDIR.${PREFIX} = ${PREFIX}/share/man 8 | MANDIR./usr/local = /usr/local/man 9 | MANDIR. = /usr/share/man 10 | MANDIR ?= ${MANDIR.${PREFIX}} 11 | 12 | .PHONY: all install test distrib 13 | 14 | all: target/release/pizauth 15 | 16 | target/release/pizauth: 17 | cargo build --release 18 | 19 | RUNNINGSYSTEMD=$(shell test -d /run/systemd/system/ && echo yes || echo no) 20 | ifeq ($(USESYSTEMD), 0) 21 | INSTALLSYSTEMD := 22 | else ifneq ($(RUNNINGSYSTEMD), yes) 23 | INSTALLSYSTEMD := 24 | else 25 | INSTALLSYSTEMD := install-systemd 26 | endif 27 | 28 | install: target/release/pizauth ${INSTALLSYSTEMD} 29 | install -d ${DESTDIR}${BINDIR} 30 | install -c -m 555 target/release/pizauth ${DESTDIR}${BINDIR}/pizauth 31 | install -d ${DESTDIR}${MANDIR}/man1 32 | install -d ${DESTDIR}${MANDIR}/man5 33 | install -c -m 444 pizauth.1 ${DESTDIR}${MANDIR}/man1/pizauth.1 34 | install -c -m 444 pizauth.conf.5 ${DESTDIR}${MANDIR}/man5/pizauth.conf.5 35 | install -d ${DESTDIR}${EXAMPLESDIR}/pizauth 36 | install -c -m 444 examples/pizauth.conf ${DESTDIR}${EXAMPLESDIR}/pizauth/pizauth.conf 37 | install -d ${DESTDIR}${SHAREDIR}/bash-completion/completions 38 | install -c -m 444 share/bash/completion.bash ${DESTDIR}${SHAREDIR}/bash-completion/completions/pizauth 39 | install -d ${DESTDIR}${SHAREDIR}/fish/vendor_completions.d 40 | install -c -m 444 share/fish/pizauth.fish ${DESTDIR}${SHAREDIR}/fish/vendor_completions.d 41 | 42 | install-systemd: 43 | install -d ${DESTDIR}${LIBDIR}/systemd/user 44 | install -c -m 444 lib/systemd/user/pizauth.service ${DESTDIR}${LIBDIR}/systemd/user/pizauth.service 45 | install -c -m 444 lib/systemd/user/pizauth-state-creds.service ${DESTDIR}${LIBDIR}/systemd/user/pizauth-state-creds.service 46 | install -c -m 444 lib/systemd/user/pizauth-state-age.service ${DESTDIR}${LIBDIR}/systemd/user/pizauth-state-age.service 47 | install -c -m 444 lib/systemd/user/pizauth-state-gpg.service ${DESTDIR}${LIBDIR}/systemd/user/pizauth-state-gpg.service 48 | install -c -m 444 lib/systemd/user/pizauth-state-gpg-passphrase.service ${DESTDIR}${LIBDIR}/systemd/user/pizauth-state-gpg-passphrase.service 49 | install -d ${DESTDIR}${EXAMPLESDIR}/pizauth 50 | install -c -m 444 examples/pizauth-state-custom.service ${DESTDIR}${EXAMPLESDIR}/pizauth/pizauth-state-custom.service 51 | 52 | test: 53 | cargo test 54 | cargo test --release 55 | 56 | distrib: 57 | test "X`git status --porcelain`" = "X" 58 | @read v?'pizauth version: ' \ 59 | && mkdir pizauth-$$v \ 60 | && cp -rp Makefile build.rs Cargo.lock Cargo.toml \ 61 | COPYRIGHT LICENSE-APACHE LICENSE-MIT \ 62 | CHANGES.md README.md README.systemd.md \ 63 | pizauth.1 pizauth.conf.5 \ 64 | examples lib share src \ 65 | pizauth-$$v \ 66 | && tar cfz pizauth-$$v.tgz pizauth-$$v \ 67 | && rm -rf pizauth-$$v 68 | -------------------------------------------------------------------------------- /README.systemd.md: -------------------------------------------------------------------------------- 1 | # Systemd unit 2 | 3 | Pizauth comes with a systemd unit. In order for it to communicate properly with 4 | `systemd`, your `startup_cmd` in `pizauth.conf` must at some point run 5 | `systemd-notify --ready --pid=parent` -- this will tell `systemd` that `pizauth` 6 | has started up. 7 | 8 | To start pizauth: 9 | 10 | ```sh 11 | $ systemctl --user start pizauth.service 12 | ``` 13 | 14 | If you want `pizauth` to start on login, run 15 | 16 | ```sh 17 | $ systemctl --user enable pizauth.service 18 | ``` 19 | 20 | (pass `--now` to also start `pizauth` with this invocation) 21 | 22 | If you want to save pizauth's dumps encrypted and automatically restore them 23 | when pizauth is started, you need to start/enable one of the 24 | `pizauth-state-*.service` files provided by pizauth. For example, 25 | 26 | ```sh 27 | $ systemctl --user enable pizauth-state-creds.service 28 | ``` 29 | 30 | Some of these units require further configuration, eg for setting the public key 31 | and location of the private key to use for encryption. For this purpose, 32 | 33 | ```sh 34 | $ systemctl --user edit pizauth-state-$METHOD.service 35 | ``` 36 | 37 | will open an editor in which you can configure your local edits to 38 | `pizauth-state-$METHOD.service`. For example, you can override the default 39 | location of the pizauth dumps (`$XDG_STATE_HOME/pizauth-state-$METHOD.dump`) to 40 | be `~/.pizauth.dump` by inserting the following line in the `.conf` file that 41 | `systemctl` will open: 42 | 43 | ```ini 44 | Environment="PIZAUTH_STATE_FILE=%h/.pizauth.dump 45 | ``` 46 | 47 | See `systemd.unit(5)` for supported values of these % "specifiers". 48 | 49 | The provided configurations are: 50 | - `pizauth-state-creds.service`: Uses `systemd-creds` to encrypt the dumps with 51 | some combination of your device's TPM2 chip and a secret accessible only to 52 | `root`. This means the dumps generally can only be decrypted *on the device 53 | that encrypted them*. 54 | - `pizauth-state-age.service`: Uses `age` to encrypt the dumps. 55 | Needs the `Environment="PIZAUTH_KEY_ID="` line to be set to the public key to 56 | encrypt with. 57 | - `pizauth-state-gpg.service`: Uses `gpg` to encrypt the dumps. 58 | Needs the `Environment="PIZAUTH_KEY_ID="` line to be set to the public key to 59 | encrypt with. `gpg-agent` will prompt for the passphrase to unlock the key, 60 | which may be undesireable in nongraphical environments. 61 | - `pizauth-state-gpg-passphrase.service`: Uses `gpg` to encrypt the dumps. 62 | Uses `systemd-creds` to encrypt a file containing the passphrase, which is set 63 | by default to be `$XDG_CONFIG_HOME/pizauth-state-gpg-passphrase.cred`. 64 | Needs the `Environment="PIZAUTH_KEY_ID="` line to be set to the public key to 65 | encrypt with. Also needs the passphrase to be stored encrypted somewhere, see 66 | the unit file for details. 67 | 68 | Note: Given the security implications here, this method is likely not much 69 | more secure than just using `pizauth-state-creds.service` directly. 70 | This unit is provided mostly to document how one might go about automatically 71 | passing key material relatively safely to a unit. 72 | -------------------------------------------------------------------------------- /src/config.y: -------------------------------------------------------------------------------- 1 | %start TopLevels 2 | %avoid_insert "STRING" 3 | %epp TIME "