|
| 1 | +import gleam/bit_array |
| 2 | +import gleam/dynamic |
| 3 | +import gleam/http/request |
| 4 | +import gleam/http/response |
| 5 | +import gleam/int |
| 6 | +import gleam/json |
| 7 | +import gleam/list |
| 8 | +import gleam/option.{None, Some} |
| 9 | +import gleam/result.{try} |
| 10 | +import gleam/string |
| 11 | +import gleam/uri.{Uri} |
| 12 | +import midas/task as t |
| 13 | +import snag |
| 14 | + |
| 15 | +pub type App { |
| 16 | + App(client_id: String, redirect_uri: String) |
| 17 | +} |
| 18 | + |
| 19 | +const auth_host = "accounts.google.com" |
| 20 | + |
| 21 | +const auth_path = "/o/oauth2/auth" |
| 22 | + |
| 23 | +pub fn authenticate(app, scopes) { |
| 24 | + let App(client_id, redirect_uri) = app |
| 25 | + let state = int.to_string(int.random(1_000_000_000)) |
| 26 | + let url = auth_url(client_id, redirect_uri, scopes, state) |
| 27 | + use redirect <- t.do(t.follow(url)) |
| 28 | + use #(access_token, token_type, returned_state) <- t.try( |
| 29 | + auth_redirect(redirect) |> result.map_error(snag.new), |
| 30 | + ) |
| 31 | + use Nil <- t.try(case returned_state == state { |
| 32 | + True -> Ok(Nil) |
| 33 | + False -> Error(snag.new("returned state was not equal to sent state")) |
| 34 | + }) |
| 35 | + |
| 36 | + use Nil <- t.try(case token_type == "Bearer" { |
| 37 | + True -> Ok(Nil) |
| 38 | + False -> Error(snag.new("returned token_type was not 'Bearer'")) |
| 39 | + }) |
| 40 | + t.Done(access_token) |
| 41 | +} |
| 42 | + |
| 43 | +fn auth_url(client_id, redirect_uri, scopes, state) { |
| 44 | + let query = [ |
| 45 | + #("client_id", client_id), |
| 46 | + #("response_type", "token"), |
| 47 | + #("redirect_uri", redirect_uri), |
| 48 | + #("state", state), |
| 49 | + #("scope", string.join(scopes, " ")), |
| 50 | + ] |
| 51 | + let query = Some(uri.query_to_string(query)) |
| 52 | + Uri(Some("https"), None, Some(auth_host), None, auth_path, query, None) |
| 53 | + |> uri.to_string |
| 54 | +} |
| 55 | + |
| 56 | +pub fn auth_redirect(redirect) { |
| 57 | + let Uri(fragment: fragment, ..) = redirect |
| 58 | + use hash <- try(case fragment { |
| 59 | + Some(hash) -> Ok(hash) |
| 60 | + None -> Error("uri did not have a fragment") |
| 61 | + }) |
| 62 | + use parts <- try( |
| 63 | + uri.parse_query(hash) |
| 64 | + |> result.replace_error("Failed to parse query: " <> hash), |
| 65 | + ) |
| 66 | + use access_token <- try(key_find(parts, "access_token")) |
| 67 | + use token_type <- try(key_find(parts, "token_type")) |
| 68 | + use returned_state <- try(key_find(parts, "state")) |
| 69 | + // other parts expires_in and scope |
| 70 | + Ok(#(access_token, token_type, returned_state)) |
| 71 | +} |
| 72 | + |
| 73 | +fn key_find(items, key) { |
| 74 | + list.key_find(items, key) |
| 75 | + |> result.replace_error("Did not find key: " <> key) |
| 76 | +} |
| 77 | + |
| 78 | +const openid_host = "openidconnect.googleapis.com" |
| 79 | + |
| 80 | +const userinfo_path = "/v1/userinfo" |
| 81 | + |
| 82 | +pub fn userinfo(token) { |
| 83 | + let request = userinfo_request(token) |
| 84 | + use response <- t.do(t.fetch(request)) |
| 85 | + use response <- t.try(userinfo_response(response)) |
| 86 | + t.Done(response) |
| 87 | +} |
| 88 | + |
| 89 | +pub fn userinfo_request(token) { |
| 90 | + request.new() |
| 91 | + |> request.set_host(openid_host) |
| 92 | + |> request.prepend_header("Authorization", string.append("Bearer ", token)) |
| 93 | + |> request.set_path(userinfo_path) |
| 94 | + |> request.set_body(<<>>) |
| 95 | +} |
| 96 | + |
| 97 | +pub fn userinfo_response(response: response.Response(BitArray)) { |
| 98 | + use json <- try( |
| 99 | + bit_array.to_string(response.body) |
| 100 | + |> result.replace_error(snag.new("not utf8 encoded")), |
| 101 | + ) |
| 102 | + use message <- try( |
| 103 | + json.decode_bits(response.body, dynamic.field("email", dynamic.string)) |
| 104 | + |> result.map_error(fn(reason) { |
| 105 | + snag.new(string.inspect(reason)) |
| 106 | + |> snag.layer("failed to decode message") |
| 107 | + }), |
| 108 | + ) |
| 109 | + Ok(message) |
| 110 | +} |
0 commit comments