Optional route authentication in Ktor - kotlin

I have a route in my Ktor application that I want to optionally authenticate. What is the best way to go about this? If I put two routing blocks in, both calls default to the unauthenticated one.
e.g.
routing {
post("/my-route") {
val request = call.receive<MyRouteRequest>()
...
}
authenticate(Constants.myAuthScope) {
post("/my-route") {
val request = call.receive<MyRouteRequest>()
val user = call.principal<User>()
...
}
}

It should be possible using more explicit models combined with validation of those either in the route or perhaps in the underlying service (depends if this is seen as domain logic or API logic)
For basic auth it looks a bit like:
sealed interface PrincipalResult {
data class User(/* ... */): PrincipalResult
object NoUserProvided: PrincipalResult
// This might be replaced with a null result to conform with the Ktor API
// I prefer making it explicit and communicate what's going on
// and not just accept a null that means everything and nothing.
//
// This can also be made into a data class and expanded
// with additional information, allowing for better errors and richer debugging
object InvalidUserCredentials: PrincipalResult
}
install(Authentication) {
basic("stuart-auth") {
realm = "Access to the '/' path"
validate { credentials ->
if (credentials.isMissing()) {
PrincipalResult.NoUserProvided
} else if (credentials.isValid() {
PrincipalResult.User(/* ... */)
} else {
PrincipalResult.InvalidUserCredentials
}
}
}
}
now one can do:
authenticate(Constants.myAuthScope) {
post("/my-route") {
val request = call.receive<MyRouteRequest>()
val principalResult = call.principal<PrincipalResult>()
when (principalResult) {
is PrincipalResult.User ->
is PrincipalResult.NoUserProvided ->
is PrincipalResult.InvalidUserCredentials ->
}
// ...
}
}
This pattern should of course be applied to whichever authentication scheme you actually use, such as JWT, OAuth, LDAP etc.

Related

Change auth credentials in KTor client

How can I change the credentials in a KTor client?
The Auth feature needs to be installed when the client is created. I've tried doing it later but it seems not to work, either as a first time setup or a repeat.
The docs suggest holding onto the client once it's created as the setup is expensive, so it seems unduly restrictive not to be able to change the credentials (& surely the smart folks at JetBrains wouldn't have done that).
I have an answer which works, to my surprise, but I'm not sure it's a good answer. Comments welcome.
Because the docs say that creating the client is expensive I've put it in a singleton and then I've done something like this
#ThreadLocal
Object ServerLink {
fun setClient(id:String, pw:String) {
// Create the client here and set id and pw
}
}
Then I simply call ServerLink.setClient(newId, newPW) whenever I want. Yes this works, and I didn't think I'd got multiple threads, but won't this be a memory leak, or at least memory waste?
You can do it by getting a reference to Auth feature and mutating its list of providers. Here is an example of changing Basic authentication credentials after client creation:
val client = HttpClient(CIO) {
install(Auth) {
basic {
username = "user"
password = "password"
}
}
}
val auth = client.feature(Auth)
if (auth != null) {
auth.providers.removeAt(0)
auth.basic {
username = "new-user"
password = "new-password"
}
}
val r = client.get<String>("http://httpbin.org/basic-auth/new-user/new-password")
println(r)
With ktor version 2.2.1 I managed to update the bearer token credentials like this:
const val REFRESH_TOKEN = "" // not required
val client = HttpClient(CIO) {
Auth {
bearer {
sendWithoutRequest { true }
loadTokens { BearerTokens(initialToken, REFRESH_TOKEN) }
}
}
}
fun updateBearerCredentials(newToken: String) {
client.plugin(Auth).bearer {
loadTokens { BearerTokens(newToken, REFRESH_TOKEN) }
}
}
I assume the same works for basic auth. Have not tested it though.

How to dynamically choose which authentication method is used in Ktor?

I implemented google sign-in in my application like so:
fun Application.module(testing: Boolean = false) {
install(CallLogging)
install(ContentNegotiation) {
gson {
setPrettyPrinting()
}
}
val jwtIssuer = environment.config.property("jwt.domain").getString()
val jwtAudience = environment.config.property("jwt.audience").getString()
val jwtRealm = environment.config.property("jwt.realm").getString()
val jwkProvider = JwkProviderBuilder(URL("https://www.googleapis.com/oauth2/v3/certs"))
.cached(10, 24, TimeUnit.HOURS)
.rateLimited(10, 1, TimeUnit.MINUTES)
.build()
install(Authentication) {
jwt {
verifier(jwkProvider) {
withIssuer(jwtIssuer)
withAudience(jwtAudience)
}
realm = jwtRealm
validate { credentials ->
if (credentials.payload.audience.contains(jwtAudience))
JWTPrincipal(credentials.payload)
else
null
}
}
}
routing {
authenticate {
post("/token-sign-in") {
val payload = call.principal<JWTPrincipal>()?.payload ?: error("JWTPrincipal not found")
call.respond(
UserWire(
id = payload.subject,
email = payload.getClaim("email").asString(),
name = payload.getClaim("name").asString(),
profilePictureUrl = payload.getClaim("picture").asString()
)
)
}
}
}
}
I want to authenticate the user every single time they access one of the routes, but I want to have both google and firebase-auth login as an option. The thing is that they require different methods to check the authenticity of the given token, hence I need two authentication methods.
I was thinking of including an "AuthenticationProvider: "Google|Firebase"" in the header of the call, and according to its value, I would decide which authentication method should be called.
So something like this:
fun Application.module(testing: Boolean = false) {
install(Authentication) {
jwt("google") {
// verify google sign in token
}
jwt("firebase") {
// verify firebase token
}
firebaseOrGoogle("firebaseOrGoogle") {
// check header value for auth provider
// verify token with either "firebase" or "google" auth methods
}
}
routing {
authenticate("firebaseOrGoogle") {
post("/token-sign-in") {
// ...
}
get("/transactions") {
// ...
}
}
}
}
Is this at all possible?
If this is possible please could you provide some code as to how to dynamically decide which authentication method should be called?
As an alternative solution, you can configure an authentication feature to try proving the identity of a user by both methods. The first successful check wins. To do that just pass those two configuration names to the authenticate method:
routing {
authenticate("google", "firebase") {
post("/token-sign-in") {
// ...
}
get("/transactions") {
// ...
}
}
}
The order of arguments determines what check comes first.

How does a view obtain data using a view model and Network API

I'm trying to fetch some data with this helper file:
https://gist.github.com/jbfbell/e011c5e4c3869584723d79927b7c4b68
Here's a snippet of the important code:
Class
/// Base class for requests to the Alpha Vantage Stock Data API. Intended to be subclasssed, but can
/// be used directly if library does not support a new api.
class AlphaVantageRequest : ApiRequest {
private static let alphaApi = AlphaVantageRestApi()
let method = "GET"
let path = ""
let queryStringParameters : Array<URLQueryItem>
let api : RestApi = AlphaVantageRequest.alphaApi
var responseJSON : [String : Any]? {
didSet {
if let results = responseJSON {
print(results)
}
}
}
}
Extension ApiRequest
/// Makes asynchronous call to fetch response from server, stores response on self
///
/// - Returns: self to allow for chained method calls
public func callApi() -> ApiRequest {
guard let apiRequest = createRequest() else {
print("No Request to make")
return self
}
let session = URLSession(configuration: URLSessionConfiguration.ephemeral)
let dataTask = session.dataTask(with: apiRequest) {(data, response, error) in
guard error == nil else {
print("Error Reaching API, \(String(describing: apiRequest.url))")
return
}
self.receiveResponse(data)
}
dataTask.resume()
return self
}
My goal is to fetch the data from responseJSON after the data of the url request is loaded.
My ViewModel currently looks like this:
class CompanyViewModel: ObservableObject {
var companyOverviewRequest: ApiRequest? {
didSet {
if let response = companyOverviewRequest?.responseJSON {
print(response)
}
}
}
private var searchEndpoint: SearchEndpoint
init(companyOverviewRequest: AlphaVantageRequest? = nil,
searchEndpoint: SearchEndpoint) {
self.companyOverviewRequest = CompanyOverviewRequest(symbol: searchEndpoint.symbol)
}
func fetchCompanyOverview() {
guard let request = self.companyOverviewRequest?.callApi() else { return }
self.companyOverviewRequest = request
}
}
So in my ViewModel the didSet gets called once but not when it should store the data. The results of AlphaVantageRequest always prints out properly, but not in my ViewModel. How can I achieve to have the loaded data also in my ViewModel?
When you use a view model which is an ObservableObject, your view wants to observe published properties, usually a viewState (MVVM terminology):
class CompanyViewModel: ObservableObject {
enum ViewState {
case undefined
case value(Company)
}
#Published var viewState: ViewState = .undefined
viewState completely describes how your view will be rendered. Note, that it can be undefined - which your view should be able to handle.
Adding a loading(Company?) case would also be a good idea. Your view can then render a loading indicator. Note that loading also provides an optional company value. You can then render a "refresh", in which case you already have a company value while also drawing a loading indicator.
In order to fetch some data from an endpoint, you may use the following abstraction:
public protocol HTTPClient: class {
func publisher(for request: URLRequest) -> AnyPublisher<HTTPResponse, Swift.Error>
}
This can be implemented by a simple wrapper around URLSession with 5 lines of code. A conforming type may however do much more: it may handle authentication, authorization, it may retry requests, refresh access tokens, or present user interfaces where the user needs to authenticate, etc. This simple protocol is sufficient for all this.
So, how does your ViewModel get the data?
It makes sense to introduce another abstraction: "UseCase" which performs this task, and not let the view model directly use the HTTP client.
A "use case" is simply an object that performs a task, taking an input and producing an output or error. You can name it how you want, "DataProvider", "ContentProvider" or something like this. "Use Case" is a well known term, though.
Conceptually, it has a similar API as an HTTP client, but semantically it sits on a higher level:
public protocol UseCase {
associatedtype Input: Encodable
associatedtype Output: Decodable
associatedtype Error
func callAsFunction(with input: Input) -> AnyPublisher<Output, Error>
}
Lets create us a "GetCompany" use case:
struct Company: Codable {
var name: String
var id: Int
}
struct GetCompanyUseCase: UseCase {
typealias Input = Int
typealias Output = Company
typealias Error = Swift.Error
private let httpClient: HTTPClient
init(httpClient: HTTPClient) {
self.httpClient = httpClient
}
func callAsFunction(with id: Int) -> AnyPublisher<Company, Swift.Error> {
let request = composeURLRequest(input: id)
return httpClient.publisher(for: request)
.tryMap { httpResponse in
switch httpResponse {
case .success(_, let data):
return data
default:
throw "invalid status code"
}
}
.decode(type: Company.self, decoder: JSONDecoder())
.map { $0 } // no-op, usually you receive a "DTO.Company" value and transform it into your Company type.
.eraseToAnyPublisher()
}
private func composeURLRequest(input: Int) -> URLRequest {
let url = URL(string: "https://api.my.com/companies?id=\(input)")!
return URLRequest(url: url)
}
}
So, this Use Case clearly accesses our HTTP client. We can implement this accessing CoreData, or read from file, or using a mock, etc. The API is always the same, and the view model does not care. The beauty here is, you can switch it out and swap in another one, the view model still works and also your view. (In order to make this really cool, you would create a AnyUseCase generic type, which is very easy, and here you have your dependency injection).
Now lets see how the view model may look like and how it uses the Use Case:
class CompanyViewModel: ObservableObject {
enum ViewState {
case undefined
case value(Company)
}
#Published var viewState: ViewState = .undefined
let getCompany: GetCompanyUseCase
var getCompanyCancellable: AnyCancellable?
init(getCompany: GetCompanyUseCase) {
self.getCompany = getCompany
}
func load() {
self.getCompanyCancellable =
self.getCompany(with: 1)
.sink { (completion) in
print(completion)
} receiveValue: { (company) in
self.viewState = .value(company)
print("company set to: \(company)")
}
}
}
The load function triggers the use case, which calls the underlying http client to load the company data.
When the UseCase returns a company, it will be assigned the view state. Observers (the view, or ViewController) will get notified about the change and can preform an update.
You can experiment with code in playground. Here are the missing peaces:
import Foundation
import Combine
extension String: Swift.Error {}
public enum HTTPResponse {
case information(response: HTTPURLResponse, data: Data)
case success(response: HTTPURLResponse, data: Data)
case redirect(response: HTTPURLResponse, data: Data)
case clientError(response: HTTPURLResponse, data: Data)
case serverError(response: HTTPURLResponse, data: Data)
case custom(response: HTTPURLResponse, data: Data)
}
class MockHTTPClient: HTTPClient {
func publisher(for request: URLRequest) -> AnyPublisher<HTTPResponse, Swift.Error> {
let json = #"{"id": 1, "name": "Some Corporation"}"#.data(using: .utf8)!
let url = URL(string: "https://api.my.com/companies")!
let httpUrlResponse = HTTPURLResponse(url: url, statusCode: 200, httpVersion: nil, headerFields: nil)!
let response: HTTPResponse = .success(response: httpUrlResponse, data: json)
return Just(response)
.mapError { _ in "no error" }
.eraseToAnyPublisher()
}
}
Assemble:
let httpClient = MockHTTPClient()
let getCompany = GetCompany(httpClient: httpClient)
let viewModel = CompanyViewModel(getCompany: getCompany)
viewModel.load()

Spring reactive web client REST request with oauth token in case of 401 response

I wanted to play around with Spring reactive web client and an actually simple example: Ask for a REST resource and in case of a 401 response get new OAuth access token.
The first part seemed to be easy:
return webClientBuilder
.baseUrl(targetInstance.getBaseUrl())
.build()
.get().uri(targetInstance.getItemEndpointUrl())
.retrieve()
.bodyToMono(ItemResponse.class)
....
But here the confusion already started. I tried something like
.onStatus(HttpStatus::is4xxClientError, (response) -> {
if(response.rawStatusCode() == 401) {
oAuthClient.initToken()
My token should then be saved within an instance JPA entity. But I have a lack of conceptual understanding here I guess. When the OAuth client receives the OAuth response I need to extract it first to persist it (as embedded object) within my instance entity. And therefore I need to block it, right?
.exchangeToMono(response -> {
if (response.statusCode().equals(HttpStatus.OK)) {
OAuthResponse oauthResponse = response.bodyToMono(OAuthResponse.class).block();
}
Based on the response result of the OAuth client I need some kind of Mono to tell the actual REST client then if it should start a retry? And which way should be the preferred on: .retrieve() or .exchangeToMono()? So I'm a bit lost here if I'm on the right path or if something like that should better be done with the classic RestTemplate? But I've also read that the RestTemplate is no deprecated...
Thanks for sharing some thoughts with me.
Ok, in the meantime I've found a non-blocking way. Maybe not the best, but it works out well for me.
The client:
class ApiClient {
public Mono<MyResponse> getResponse(Tenant tenant) {
return webClientBuilder
.baseUrl(tenant.getUrl())
.clientConnector(getClientConnector())
.build()
.get().uri("/api/my-content-entpoint")
.exchangeToMono(response -> {
if (response.statusCode().equals(HttpStatus.OK)) {
return response.bodyToMono(MyResponse.class);
} else if(response.statusCode().equals(HttpStatus.FORBIDDEN)) {
return Mono.error(new MyOAuthExcpetion());
} else {
return Mono.empty();
}
});
}
}
the service:
#Service
public class MyService {
private final ApiClient apiClient;
private final RetryStrategy retryStrategy;
private final TenantService tenantService;
public Mono<MyResponse> getResponse(String tenantId){
return tenantService.getTenant(tenantId)
.flatMap(tenant-> apiClient.getResponse(instance))
.retryWhen(Retry.from(signals -> signals
.flatMap(retrySignal -> retryStrategy.reconnect(retrySignal, tenantId))));
}
}
and the retry strategy
#Component
public class RetryStrategy {
private final TenantService tenantService;
public Publisher<? extends Long> reconnect(RetrySignal retrySignal, String tenantId) {
long count = retrySignal.totalRetriesInARow();
Throwable failure = retrySignal.failure();
if(count > 0) {
return Mono.error(new UnsupportedOperationException("Retry failed", failure));
}
Mono<Tenant> updatedTenant = null;
if(failure instanceof MyOAuthExcpetion) {
updatedTenant = tenantService.getTenant(tenantId)
.flatMap(tenant -> tenantService.refreshOAuth(tenant));
}
if(updatedTenant == null) {
return Mono.error(new UnsupportedOperationException("Retry failed", failure));
}
return updatedTenant.then(Mono.delay(Duration.ofSeconds(1)));
}
}
Happy for any feedback or improvements.
In my application I went with prechecking the token before requests are being made:
client.get()
.uri("...")
.header("Authorization", "Bearer " + authenticator.getToken(client,token))
.retrieve()
...
And in Authenticator Service I verify the validity of the token as follow:
String getToken(WebClient client, String token) {
if (token == null || isTokenExpired(token)) {
return this.fetchToken(client); // fetches a new token
}
return token;
}
private boolean isTokenExpired(String token) {
DecodedJWT jwt = JWT.decode(token);
return jwt.getExpiresAt().before(new Date());
}

Grails 3.0.x Interceptor matchAll().excludes for multiple controllers

Following Grails 3.0.11 Interceptors document, I code my own Interceptors as below:
class AuthInterceptor {
int order = HIGHEST_PRECEDENCE;
AuthInterceptor() {
println("AuthInterceptor.AuthInterceptor(): Enter..............");
// ApiController.index() and HomeController.index() don't need authentication.
// Other controllers need to check authentication
matchAll().excludes {
match(controller:'api', action:'index);
match(controller:'home', action:'index');
}
}
boolean before() {
println "AuthInterceptor.before():Enter----------------->>>>>>";
log.debug("AuthInterceptor.before(): params:${params}");
log.debug("AuthInterceptor.before(): session.id:${session.id}");
log.debug("AuthInterceptor.before(): session.user:${session.user?.englishDisplayName}");
if (!session.user) {
log.debug("AuthInterceptor.before(): display warning msg");
render "Hi, I am gonna check authentication"
return false;
} else {
return true;
}
}
boolean after() {
log.debug("AuthInterceptor.after(): Enter ...........");
true
}
void afterView() {
// no-op
}
}
class P2mController {
def index() {
log.debug("p2m():Enter p2m()..............")
render "Hi, I am P2M";
}
}
When I test http://localhost:8080/p2m/index, from log console, I saw that P2mController.index() is executed without been checked authentication.
However, when I test http://localhost:8080/api/index or http://localhost:8080/home/index, AuthInterceptor.check() is executed and the browser displays
Hi, I am gonna check authentication
I wish P2mController been checked authentication, and HomeController.index() and ApiController.index() don't need to be checked authentication. But from the log and response, the result is opposite.
Where is wrong in my AuthInterceptor ?
You want to do this instead:
matchAll().excludes(controller:'api', action:'index')
.excludes(controller:'home', action:'index')
And don't forget the single-quote after the first 'index'.