Last active
February 23, 2022 06:13
-
-
Save P-Louw/1e8da8006160e3db20c0297d062f5d93 to your computer and use it in GitHub Desktop.
OAuth desktop client Implicit token script (Helix/Twitch)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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