Skip to content

Instantly share code, notes, and snippets.

@P-Louw
Last active February 23, 2022 06:13
Show Gist options
  • Save P-Louw/1e8da8006160e3db20c0297d062f5d93 to your computer and use it in GitHub Desktop.
Save P-Louw/1e8da8006160e3db20c0297d062f5d93 to your computer and use it in GitHub Desktop.
OAuth desktop client Implicit token script (Helix/Twitch)
#!/usr/bin/env -S dotnet fsi
open System
open System.IO
open System.Net
open System.Text
open System.Diagnostics
open System.Threading
"""
Acquire OAuth implicit flow token for a desktop clients.
Api used is the Twitch Helix api, the api offers multiple OAuth tokens.
The implicit token flow is suited for non server to server requests and
returns a token of type 'bearer' in the url fragment.
Fragment is sent to the desktop client after redirecting to a local page.
The redirect page will grab the token from the url fragment and post it to the client.
REQUIREMENTS:
- Client-id from developer console (This uses a client-id for application from twitch dev console.)
- Available port on loopback interface (localhost/127.0.0.1), ipv4 used here.
- Template post url should match redirect url, the default html template matches the deafult redirect url.
"""
[<Literal>]
let CLIENTID = "<CLIENT-ID FOR APPLICATIONS FROM DEV CONSOLE>"
[<RequireQualifiedAccess>]
module OAuthHttp =
[<Literal>]
let loopbackRedirect = @"http://localhost:3768/"
[<Literal>]
let redirectHtml =
"""
<!DOCTYPE html><html lang="en">
<head><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.3/css/bulma.min.css">
<meta charset="UTF-8"><title>.NET OAuth2 implicit token as fragment.</title></head><body><section class="hero is-primary"><div class="hero-body"><p class="title">Desktop authentication</p><a class="subtitle" href="https://gist.github.com/P-Louw/1e8da8006160e3db20c0297d062f5d93">
OAuth implicit flow token</a></div></section><section class="section is-medium"><div class='container has-text-centered'><div class='columns is-mobile is-centered'><div class='column is-5'><div class="card"><div id="resultCard" class="card-content"><?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="128px" height="128px" viewBox="-6 0 268 268" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid"><g><path d="M17.4579119,0 L0,46.5559188 L0,232.757287 L63.9826001,232.757287 L63.9826001,267.690956 L98.9144853,267.690956
L133.811571,232.757287 L186.171922,232.757287 L256,162.954193 L256,0 L17.4579119,0 Z M40.7166868,23.2632364 L232.73141,23.2632364 L232.73141,151.29179 L191.992415,192.033461 L128,192.033461 L93.11273,226.918947 L93.11273,192.033461 L40.7166868,192.033461 L40.7166868,23.2632364 Z M104.724985,139.668381
L127.999822,139.668381 L127.999822,69.843872 L104.724985,69.843872 L104.724985,139.668381 Z M168.721862,139.668381 L191.992237,139.668381 L191.992237,69.843872 L168.721862,69.843872 L168.721862,139.668381 Z" fill="#5A3E85"></path>
</g></svg><p>After the validation you can close this window, there is no need to copy or remember the token yourself.</p><p id=valid class='tag is-link is-light mt-4 mb-4 has-text-weight-bold'>Validating</p></div></div></div></div></div></section></body><script>
function sendToken (tokenStr) { fetch("http://localhost:3768/tokenGet/", { method: "post", headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' }, body: JSON.stringify({ token: tokenStr }) }) } var hashKv = window.location.hash.substring(1).split("&");
var maybeToken = hashKv[0].split("="); var resultCard = document.querySelector("#resultCard"); var statusTag = document.querySelector("#valid"); if (maybeToken[0] == "access_token") { sendToken(maybeToken[1]); statusTag.innerHTML = "Success"; statusTag.classList.add("has-text-info");
var i = document.createElement('input'); i.setAttribute("type", "text"); i.setAttribute("disabled", ''); i.value = maybeToken[1]; resultCard.append(i); } else { statusTag.innerHTML = "Client failed to receive token.";
statusTag.classList.add("has-text-danger"); }; </script></html>
"""
type OS =
| OSX
| Windows
| Linux
// Jack Mott - http://fssnip.net/7OP/title/Detect-operating-system
let getOS =
match int Environment.OSVersion.Platform with
| 4
| 128 -> Linux
| 6 -> OSX
| _ -> Windows
let launchAuthBrowser (apiAuthUrl: string) =
let validUri = new Uri(apiAuthUrl)
printfn "Launching with %s" validUrl.AbsoluteUri
let os = getOS
match os with
| OS.Linux -> Process.Start("xdg-open", validUrl.AbsoluteUri)
| OS.OSX -> Process.Start("open", validUrl.AbsoluteUri)
| OS.Windows ->
// Query seperators won't be included using proces without this workaround.
let urlForProcess = validUrl.AbsoluteUri.Replace("&", "^&");
new ProcessStartInfo("cmd", $"/c start %s{urlForProcess}",UseShellExecute = true ,CreateNoWindow = false)
|> Process.Start
type TokenResult = { token:string }
type requestConfig = { redirectUrl:string; redirectPage:string; }
type Visit =
| Redirect of string
| TokenPost of TokenResult
| Invalid of string
type OauthServer (redirectUrl: string, redirectPage: string) =
let serverToken = new CancellationTokenSource();
let handler (serverCtx: requestConfig) =
let confHandler (ctx: HttpListenerContext) =
async {
match ctx.Request.RawUrl with
| "/" ->
let page = Encoding.ASCII.GetBytes(serverCtx.redirectPage)
ctx.Response.ContentType <- "text/html"
ctx.Response.OutputStream.Write(page, 0, page.Length)
ctx.Response.OutputStream.Close()
return Redirect ctx.Request.UserHostAddress
| "/tokenGet/" ->
use sr = new StreamReader(ctx.Request.InputStream, ctx.Request.ContentEncoding)
let msg = sr.ReadToEnd()
let result = Json.JsonSerializer.Deserialize<TokenResult>(msg)
return TokenPost { token = result.token }
| _ ->
return Invalid ctx.Request.RawUrl
}
confHandler
let serverLoop (conf: requestConfig) consumer f =
async {
use listener = new HttpListener()
listener.Prefixes.Add conf.redirectUrl
listener.Start()
while listener.IsListening do
let! context = Async.FromBeginEnd(listener.BeginGetContext, listener.EndGetContext)
printfn "Received request"
let! result = f context
match result with
| TokenPost token -> consumer token; listener.Stop(); serverToken.Cancel();
| Redirect userHost -> printfn "User visited redirect page from host: %s" userHost
| Invalid path -> printfn "Did not recognize the path: %s" path
}
member this.startServer(server:requestConfig) consumer =
printfn "Starting oauth redirect server"
Async.Start((serverLoop server consumer (handler server)), serverToken.Token)
serverToken
let startOauthRequest (clientId:string) (consumer: TokenResult -> 'u) (server:requestConfig) =
let serverHost =
new OauthServer(server.redirectUrl, server.redirectPage)
let handle = serverHost.startServer server consumer
$"https://id.twitch.tv/oauth2/authorize?response_type=token&client_id={clientId}&redirect_uri={server.redirectUrl}&scope="
|> launchAuthBrowser
|> ignore
handle
let getOauthTokenDesktop clientId =
// Request implicit token using default localhost ipv4 in OAuthHttp type, clientId can be filled at the top.
OAuthHttp.createServer OAuthHttp.loopbackRedirect
|> OAuthHttp.startOauthRequest clientId (fun tokenResult -> printfn "Got the token: %A" tokenResult.token )
let req = getOauthTokenDesktop CLIENTID
req.Token.WaitHandle.WaitOne()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment