oauth2.bend relies on unsafe/foreign
raw source on the hub · import bend-kit-oauth2@0.3.0.0/oauth2.bend as Oauth2
OAuth 2.0 client credentials, refresh, and authorization code with PKCE over Hairpin. Source: https://github.com/paymog/bend-kit/tree/main/oauth2
9 imports
import Base import bend-kit-crypto@0.1.1.0/crypto.bend as Crypto import bend-kit-hairpin@0.3.0.0/hairpin.bend as Hairpin import bend-kit-http@0.32.0.0/http.bend as Http import bend-kit-time@0.1.2.1/time.bend as Time import 0x1f2d80f53f971b16c6de6a65cb1918ae/url.bend as Url import 0x584fc27920487ceab242392391418d7f/json.bend as Json import 0xcfc8be7b076f41f95c8e118383892d55/encoding.bend as Enc import 0x49814d83de8f70993a43e1002be29ecd/bytes.bend as Bytes
Types
type Config source · line 16 · raw
Data
RFC 6749 and RFC 7636. Text is a String of code points; form values go out as UTF-8. token: the token endpoint URL. secret: "" for a public client, which sends client_id in the body; a confidential client authenticates with HTTP Basic (RFC 6749 §2.3.1).
Config@token:String -> @id:String -> @secret:String -> Config
type Token source · line 21 · raw
Data
kind: token_type, as "Bearer". refresh: "" for none. scope: "" when the server did not say. expires: when the access token lapses, counted from just before the request; None without expires_in.
Token@access:String -> @kind:String -> @refresh:String -> @scope:String -> @expires:Maybe<&2, 0xe00bbf72c3ff9dc6d7b077524522b3a8/time.Instant> -> Token
type Err source · line 28 · raw
Data
ErrNet: the request did not complete. ErrOAuth: an RFC 6749 §5.2 error with its status, or a §4.1.2.1 error in a callback, status 0. ErrReply: not a Bearer token response, with its status and text. ErrState: the callback's state is not ours (§10.12). ErrCrypto: the random source or SHA-256 failed. ErrExpired: the token lapsed and has no refresh token.
ErrNet@e:0x5f997a9351e7133e43ba9611e734f8a6/hairpin.Err -> Err
ErrOAuth@status:U32 -> @code:String -> @why:String -> Err
ErrReply@status:U32 -> @body:String -> Err
ErrStateErr
ErrCrypto@code:U32 -> @why:String -> Err
ErrExpiredErr
type Got source · line 91 · raw
Data
Got@access:String -> @kind:String -> @refresh:String -> @scope:String -> @expires:String -> @error:String -> @desc:String -> Got
Definitions
def b64url.ch source · line 38 · raw
@+c:U32 -> @t:String -> String
def b64url.go source · line 49 · raw
@s:String -> String
def b64url source · line 57 · raw
@b:0x49814d83de8f70993a43e1002be29ecd/bytes.Bytes -> String
Base64url without padding (RFC 4648 §5, RFC 7636 Appendix A).
def utf8 source · line 62 · raw
@+s:String -> String
def field source · line 65 · raw
@m:Map<&2, String> -> @k:String -> @+v:String -> Map<&2, String>
def scoped source · line 68 · raw
@+scope:String -> @+m:Map<&2, String> -> Map<&2, String>
def form source · line 71 · raw
@+k:String -> @+v:String -> Map<&2, String>
def basic source · line 75 · raw
@+id:String -> @+secret:String -> String
RFC 6749 §2.3.1: id and secret are form-encoded before base64.
def auth.headers source · line 78 · raw
@+id:String -> @+secret:String -> Map<&2, List<&2, String>>
def auth.body source · line 82 · raw
@+id:String -> @+secret:String -> @+m:Map<&2, String> -> Map<&2, String>
def body source · line 86 · raw
@+id:String -> @+secret:String -> @m:Map<&2, String> -> String
The form body of a token request from cfg's client.
def got.put source · line 94 · raw
@g:Got -> @+k:String -> @+v:String -> Got
def got.text source · line 102 · raw
@v:0x584fc27920487ceab242392391418d7f/json.Val -> String
A string's text, or a number's, as some servers send expires_in as "3600".
def got.go source · line 111 · raw
@kvs:List<&1, Pair(0x49814d83de8f70993a43e1002be29ecd/bytes.Bytes, 0x584fc27920487ceab242392391418d7f/json.Val)> -> @g:Got -> Got
def got.obj source · line 118 · raw
@v:0x584fc27920487ceab242392391418d7f/json.Val -> Maybe<&2, Got>
def got source · line 125 · raw
@m:Maybe<&1, 0x584fc27920487ceab242392391418d7f/json.Val> -> Maybe<&2, Got>
def expiry source · line 132 · raw
@now:0xe00bbf72c3ff9dc6d7b077524522b3a8/time.Instant -> @m:Maybe<&2, U32> -> Maybe<&2, 0xe00bbf72c3ff9dc6d7b077524522b3a8/time.Instant>
def good source · line 140 · raw
@+status:U32 -> @g:Got -> Bool
A 2xx with an access token of type Bearer (RFC 6750 §4, case-insensitive), the only kind request sends.
def token.pick source · line 145 · raw
@ok:Bool -> @+status:U32 -> @+old:String -> @now:0xe00bbf72c3ff9dc6d7b077524522b3a8/time.Instant -> @b:0x49814d83de8f70993a43e1002be29ecd/bytes.Bytes -> @g:Got -> Result<&1, &1, Err, Token>
§6: a refresh reply without refresh_token keeps the old one. b: the body, decoded only for an error.
def token.of source · line 154 · raw
@+status:U32 -> @+old:String -> @now:0xe00bbf72c3ff9dc6d7b077524522b3a8/time.Instant -> @b:0x49814d83de8f70993a43e1002be29ecd/bytes.Bytes -> @m:Maybe<&2, Got> -> Result<&1, &1, Err, Token>
def parse.copy source · line 161 · raw
@+status:U32 -> @+old:String -> @now:0xe00bbf72c3ff9dc6d7b077524522b3a8/time.Instant -> @r:Pair(0x49814d83de8f70993a43e1002be29ecd/bytes.Bytes, 0x49814d83de8f70993a43e1002be29ecd/bytes.Bytes) -> Result<&1, &1, Err, Token>
def parse source · line 166 · raw
@+status:U32 -> @+old:String -> @now:0xe00bbf72c3ff9dc6d7b077524522b3a8/time.Instant -> @b:0x49814d83de8f70993a43e1002be29ecd/bytes.Bytes -> Result<&1, &1, Err, Token>
A token endpoint's reply at status, requested at now. old: the refresh token to keep when none comes back.
def reply source · line 169 · raw
@+old:String -> @now:0xe00bbf72c3ff9dc6d7b077524522b3a8/time.Instant -> @r:Result<&1, &1, 0x5f997a9351e7133e43ba9611e734f8a6/hairpin.Err, 0x6a946b742ab6f76f0ba5b90b14da52ad/http.Res> -> Result<&1, &1, Err, Token>
def grant.done source · line 177 · raw
@+old:String -> @now:0xe00bbf72c3ff9dc6d7b077524522b3a8/time.Instant -> @x:Pair(0x5f997a9351e7133e43ba9611e734f8a6/hairpin.Client, Result<&1, &1, 0x5f997a9351e7133e43ba9611e734f8a6/hairpin.Err, 0x6a946b742ab6f76f0ba5b90b14da52ad/http.Res>) -> Pair(0x5f997a9351e7133e43ba9611e734f8a6/hairpin.Client, Result<&1, &1, Err, Token>)
def grant source · line 183 · raw
@h:0x5f997a9351e7133e43ba9611e734f8a6/hairpin.Client -> @cfg:Config -> @m:Map<&2, String> -> @+old:String -> IO(Pair(0x5f997a9351e7133e43ba9611e734f8a6/hairpin.Client, Result<&1, &1, Err, Token>))
POST m to the token endpoint. Hairpin sends a POST once.
def client_credentials source · line 194 · raw
@h:0x5f997a9351e7133e43ba9611e734f8a6/hairpin.Client -> @cfg:Config -> @+scope:String -> IO(Pair(0x5f997a9351e7133e43ba9611e734f8a6/hairpin.Client, Result<&1, &1, Err, Token>))
RFC 6749 §4.4. scope: "" for the server's default.
def code source · line 198 · raw
@h:0x5f997a9351e7133e43ba9611e734f8a6/hairpin.Client -> @cfg:Config -> @+code:String -> @+redirect:String -> @+verifier:String -> IO(Pair(0x5f997a9351e7133e43ba9611e734f8a6/hairpin.Client, Result<&1, &1, Err, Token>))
RFC 6749 §4.1.3 with RFC 7636 §4.5: the code from callback, the same redirect_uri, and the verifier.
def refresh.go source · line 202 · raw
@none:Bool -> @h:0x5f997a9351e7133e43ba9611e734f8a6/hairpin.Client -> @cfg:Config -> @+r:String -> @+scope:String -> IO(Pair(0x5f997a9351e7133e43ba9611e734f8a6/hairpin.Client, Result<&1, &1, Err, Token>))
def refresh source · line 210 · raw
@h:0x5f997a9351e7133e43ba9611e734f8a6/hairpin.Client -> @cfg:Config -> @tok:Token -> @+scope:String -> IO(Pair(0x5f997a9351e7133e43ba9611e734f8a6/hairpin.Client, Result<&1, &1, Err, Token>))
RFC 6749 §6. scope: "" keeps the granted scope. ErrExpired when tok has no refresh token.
def expiring.at source · line 214 · raw
@e:Maybe<&2, 0xe00bbf72c3ff9dc6d7b077524522b3a8/time.Instant> -> @now:0xe00bbf72c3ff9dc6d7b077524522b3a8/time.Instant -> @+skew:U32 -> Bool
def expiring source · line 222 · raw
@tok:Token -> @now:0xe00bbf72c3ff9dc6d7b077524522b3a8/time.Instant -> @+skew:U32 -> Bool
Does tok lapse within skew seconds of now? A token without expires_in never does.
def fresh.go source · line 226 · raw
@due:Bool -> @h:0x5f997a9351e7133e43ba9611e734f8a6/hairpin.Client -> @cfg:Config -> @+tok:Token -> IO(Pair(0x5f997a9351e7133e43ba9611e734f8a6/hairpin.Client, Result<&1, &1, Err, Token>))
def fresh source · line 234 · raw
@h:0x5f997a9351e7133e43ba9611e734f8a6/hairpin.Client -> @cfg:Config -> @+tok:Token -> @+skew:U32 -> IO(Pair(0x5f997a9351e7133e43ba9611e734f8a6/hairpin.Client, Result<&1, &1, Err, Token>))
tok, or a refreshed one when it lapses within skew seconds.
def fresh.cc.go source · line 239 · raw
@due:Bool -> @h:0x5f997a9351e7133e43ba9611e734f8a6/hairpin.Client -> @cfg:Config -> @+scope:String -> @+tok:Token -> IO(Pair(0x5f997a9351e7133e43ba9611e734f8a6/hairpin.Client, Result<&1, &1, Err, Token>))
def fresh.cc source · line 247 · raw
@h:0x5f997a9351e7133e43ba9611e734f8a6/hairpin.Client -> @cfg:Config -> @+scope:String -> @+tok:Token -> @+skew:U32 -> IO(Pair(0x5f997a9351e7133e43ba9611e734f8a6/hairpin.Client, Result<&1, &1, Err, Token>))
tok, or a new client-credentials token when it lapses within skew seconds (§4.4.3: no refresh token).
def random.of source · line 254 · raw
@r:Result<&1, &1, Pair(U32, String), Pair(U32, Array<U32>)> -> Result<&1, &1, Err, String>
def random source · line 262 · raw
@+n:U32 -> IO(Result<&1, &1, Err, String>)
n octets from the OS secure random source, as base64url.
def verifier source · line 268 · raw
IO(Result<&1, &1, Err, String>)
§4.1: 32 random octets, 43 unreserved chars.
def state source · line 272 · raw
IO(Result<&1, &1, Err, String>)
An unguessable state for the authorization request (RFC 6749 §10.12).
def challenge.words source · line 275 · raw
@b:0x49814d83de8f70993a43e1002be29ecd/bytes.Bytes -> IO(Result<&1, &1, Err, String>)
def challenge source · line 282 · raw
@+verifier:String -> IO(Result<&1, &1, Err, String>)
§4.2 S256: BASE64URL(SHA256(ASCII(verifier))).
def qmark source · line 285 · raw
@s:String -> Bool
def authorize.url source · line 293 · raw
@cfg:Config -> @+endpoint:String -> @+redirect:String -> @+scope:String -> @+state:String -> @+challenge:String -> String
RFC 6749 §4.1.1 with RFC 7636 §4.3: the URL to send the user to. endpoint may carry its own query.
def param.of source · line 299 · raw
@r:Pair(Map<&2, String>, String) -> String
def param source · line 303 · raw
@+m:Map<&2, String> -> @k:String -> String
def callback.pick source · line 306 · raw
@+err:String -> @desc:String -> @+got:String -> @+code:String -> @+state:String -> Result<&1, &1, Err, String>
def callback.m source · line 311 · raw
@+state:String -> @m:Maybe<&2, 0x1f2d80f53f971b16c6de6a65cb1918ae/url.Url> -> Result<&1, &1, Err, String>
def callback.frag source · line 318 · raw
@+state:String -> @r:Pair(String, String) -> Result<&1, &1, Err, String>
def callback.q source · line 322 · raw
@+state:String -> @r:Pair(String, String) -> Result<&1, &1, Err, String>
def callback source · line 327 · raw
@url:String -> @+state:String -> Result<&1, &1, Err, String>
§4.1.2: the code in the redirect to redirect_uri, once its state matches ours; the server's error if it sent one.
def bearer source · line 332 · raw
@tok:Token -> @h:Map<&2, List<&2, String>> -> Map<&2, List<&2, String>>
def request source · line 337 · raw
@c:0x5f997a9351e7133e43ba9611e734f8a6/hairpin.Client -> @tok:Token -> @+method:String -> @url:String -> @headers:Map<&2, List<&2, String>> -> @body:0x49814d83de8f70993a43e1002be29ecd/bytes.Bytes -> IO(Pair(0x5f997a9351e7133e43ba9611e734f8a6/hairpin.Client, Result<&1, &1, 0x5f997a9351e7133e43ba9611e734f8a6/hairpin.Err, 0x6a946b742ab6f76f0ba5b90b14da52ad/http.Res>))
Hairpin.request with Authorization: Bearer <access>, over any authorization header in headers.