Skip to content

Instantly share code, notes, and snippets.

@lotusirous
Last active October 22, 2022 10:56
Show Gist options
  • Save lotusirous/fd4823482844872ea9dccd5bbe5de6d7 to your computer and use it in GitHub Desktop.
Save lotusirous/fd4823482844872ea9dccd5bbe5de6d7 to your computer and use it in GitHub Desktop.
An example of auto renew token and fetch the resource
import Combine
import Foundation
enum NetworkError: Error {
case unauthorized
case badServerResponse
case wrongType
case unknown
}
class ViewModel: ObservableObject {
@Published var responseText: String = ""
private let api = API()
private let getAdminResource = PassthroughSubject<Void, NetworkError>()
private var bag = Set<AnyCancellable>()
init() {
addSubscribers()
}
private func addSubscribers() {
getAdminResource
.flatMap { self.api.request(path: "/admin") }
.receive(on: DispatchQueue.main)
.sink(receiveCompletion: { print($0) },
receiveValue: { [weak self] data in
let b = String(data: data, encoding: .utf8) ?? ""
self?.responseText = b
})
.store(in: &bag)
}
func login() {
api.login()
}
func showBag() {
print("Inside the bag")
bag.forEach { print($0) }
print("=====")
}
}
class API {
typealias Token = String
var currentToken: Token?
let baseURL = URL(string: "http://localhost:1234")!
private let queue = DispatchQueue(label: "authenticator.sample")
private let session = URLSession(configuration: .default)
// this publisher is shared amongst all calls that request a token refresh
private var refreshPublisher: AnyPublisher<Token, NetworkError>?
func login() {
currentToken = "first login token"
}
func publisher(for url: URL, token: Token?) -> AnyPublisher<Data, NetworkError> {
var req = URLRequest(url: url)
if let token = token {
req.setValue(token, forHTTPHeaderField: "Authorization")
}
return session.dataTaskPublisher(for: req)
.tryMap { data, response in
guard let response = response as? HTTPURLResponse else {
throw NetworkError.wrongType
}
if response.statusCode == 401 {
throw NetworkError.unauthorized
}
if response.statusCode != 200 {
throw NetworkError.badServerResponse
}
return data
}
.mapError { $0 as? NetworkError ?? .unknown }
.eraseToAnyPublisher()
}
func validToken(force: Bool = false) -> AnyPublisher<Token, NetworkError> {
return queue.sync {
// case 1: we're already loading a new token
if let publisher = refreshPublisher {
return publisher
}
// scenario 2: we don't have a token at all
guard let token = currentToken else {
return Fail(error: NetworkError.unauthorized)
.eraseToAnyPublisher()
}
// case 3: we already have a valid token and dont' want to force a refresh.
if !token.isEmpty, !force {
return Just(token)
.setFailureType(to: NetworkError.self)
.eraseToAnyPublisher()
}
// case 4: we get the new token
let url = baseURL.appendingPathComponent("/token")
let publisher: AnyPublisher<Token, NetworkError> = publisher(for: url, token: nil)
.share()
.map { data in
String(data: data, encoding: .utf8) ?? ""
}
.handleEvents(receiveOutput: { token in
self.currentToken = token
}, receiveCompletion: { _ in
self.refreshPublisher = nil
})
.eraseToAnyPublisher()
refreshPublisher = publisher
return publisher
}
}
func request(path: String) -> AnyPublisher<Data, NetworkError> {
let u = baseURL.appendingPathComponent(path)
return validToken() // emit a valid token.
.flatMap { token in
self.publisher(for: u, token: token)
}
.tryCatch { error -> AnyPublisher<Data, NetworkError> in
// renew the token
if error != NetworkError.unauthorized {
throw error
}
return self.validToken(force: true)
.flatMap { tok in
self.publisher(for: u, token: tok)
}
.eraseToAnyPublisher()
}
.mapError { $0 as? NetworkError ?? .unknown }
.eraseToAnyPublisher()
}
}
package main
import (
"log"
"math/rand"
"net/http"
"time"
)
func init() {
rand.Seed(time.Now().UnixNano())
}
var letterRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
func RandStringRunes(n int) string {
b := make([]rune, n)
for i := range b {
b[i] = letterRunes[rand.Intn(len(letterRunes))]
}
return string(b)
}
var issuedTokens = make(map[string]int)
// returns an http.HandlerFunc that processes http requests to ...
func HandleIssueToken(w http.ResponseWriter, r *http.Request) {
token := RandStringRunes(10)
log.Println("issue new token: ", token)
issuedTokens[token] = 0
w.Write([]byte(token))
}
// HandleSample writes a hello message to response.
func HandleAuthorized(w http.ResponseWriter, r *http.Request) {
log.Println("get authorized resource")
tok := r.Header.Get("Authorization")
if tok == "" {
w.WriteHeader(http.StatusBadRequest)
return
}
log.Println("use token: ", tok)
used, ok := issuedTokens[tok]
if !ok {
w.WriteHeader(http.StatusUnauthorized)
w.Write([]byte("Unauthorized, please issuee a new token"))
return
}
if used > 2 {
w.WriteHeader(401)
w.Write([]byte("Token is expired"))
return
}
// dump issused token
// fmt.Println("===")
// for k, v := range issuedTokens {
// fmt.Println(k, v)
// }
// fmt.Println("===")
issuedTokens[tok]++
w.Write([]byte("Well done, server time is: " + time.Now().Format("2006-01-02 15:04:05")))
}
func main() {
addr := ":1234"
r := http.NewServeMux()
r.HandleFunc("/admin", HandleAuthorized)
r.HandleFunc("/token", HandleIssueToken)
svr := &http.Server{
Addr: addr,
Handler: r,
}
log.Println("server started", addr)
log.Fatal(svr.ListenAndServe())
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment