Last active
October 22, 2022 10:56
-
-
Save lotusirous/fd4823482844872ea9dccd5bbe5de6d7 to your computer and use it in GitHub Desktop.
An example of auto renew token and fetch the resource
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
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() | |
} | |
} |
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
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