Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
7eca10c
refactor(sql): migrate authentication to auth_provider table.
Neilk1021 Jul 13, 2026
fe18c46
refactor(sql): migrate authentication to auth_provider table.
Neilk1021 Jul 13, 2026
c17b79f
refactor(auth): refactor `GoogleAuthResource` to match new schema
Neilk1021 Jul 14, 2026
2123cc1
refactor(auth): refactor `AdminUserResource` to match new schema
Neilk1021 Jul 14, 2026
ceb05b0
fix(auth): rebind avatar to providers table instead of user.
Neilk1021 Jul 14, 2026
c263625
refactor(auth): Refactor user construction to be more robust.
Neilk1021 Jul 14, 2026
59c0392
fix(tests): spec test refactors to match shcema
Neilk1021 Jul 14, 2026
31fafd4
refactor(users): binding avatar back onto user
Neilk1021 Jul 14, 2026
0595a8b
style(users): ran scalafmt and scalafix
Neilk1021 Jul 14, 2026
d05cf98
fix(tests): remove user.setPassword from unit tests
Neilk1021 Jul 15, 2026
9c47950
refactor(front end): google is now bound to the front end!
Neilk1021 Jul 15, 2026
7b5a7a6
style(auth): ran scalafmt
Neilk1021 Jul 16, 2026
955a5d5
Merge remote-tracking branch 'upstream/main' into task/authentication…
Neilk1021 Jul 16, 2026
605f92f
merge(auth): pulled main into this branch
Neilk1021 Jul 16, 2026
fe5b8f7
temp(auth): login page and incomplete facebook backend
Neilk1021 Jul 17, 2026
8b8d5b5
feat(auth): facebook integration finished
Neilk1021 Jul 20, 2026
e7fcddb
feat(auth): Abstraction of login to `ExternalAuthProvisioner` and inc…
Neilk1021 Jul 21, 2026
a6f2834
test(Auth): Added tests to cover front and backend modifications
Neilk1021 Jul 21, 2026
c3e64a7
fix(auth): move 28.sql to 29.sql
Neilk1021 Jul 21, 2026
2942b6e
Merge branch 'main' into task/authentication-refactor
Neilk1021 Jul 21, 2026
e498d29
fix(auth): migration fun
Neilk1021 Jul 21, 2026
028e702
fix(auth): Apache Headers
Neilk1021 Jul 21, 2026
b68ad93
style(auth): ran prettier and lint
Neilk1021 Jul 21, 2026
a06f570
fix(auth): fix frontend tests
Neilk1021 Jul 21, 2026
596ec59
refactor(sql): combine provider_id and password into "secret"
Neilk1021 Jul 24, 2026
5d19439
refactor(auth): migrate local auth to auth_provider
Neilk1021 Jul 27, 2026
1c0c3d2
Merge branch 'task/authentication-refactor' of https://github.com/Nei…
Neilk1021 Jul 27, 2026
14821fe
Merge branch 'apache:main' into task/authentication-refactor
Neilk1021 Jul 27, 2026
ef0bca6
refactor(auth): fix migrations
Neilk1021 Jul 27, 2026
2329c50
fix(auth): migrate google functionality / fix logout on external auth…
Neilk1021 Jul 28, 2026
a963f1e
Merge branch 'apache:main' into task/authentication-refactor
Neilk1021 Jul 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,6 @@ class AccessControlResourceSpec
user.setName("testuser")
user.setEmail("test@example.com")
user.setRole(UserRoleEnum.REGULAR)
user.setPassword("password")
user
}

Expand All @@ -76,7 +75,6 @@ class AccessControlResourceSpec
user.setName("testuser2")
user.setEmail("test2@example.com")
user.setRole(UserRoleEnum.REGULAR)
user.setPassword("password")
user
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,18 @@ import org.scalatest.matchers.should.Matchers

import java.security.Principal
import java.util.concurrent.ConcurrentLinkedQueue
import scala.util.chaining.scalaUtilChainingOps

class UserActivityEventListenerSpec extends AnyFlatSpec with Matchers {

private def sessionUser(uid: Integer): SessionUser = {
val u = new User(uid, "u", null, null, null, null, UserRoleEnum.REGULAR, null, null, null, null)
new SessionUser(u)
new SessionUser({
new User().tap { u =>
u.setUid(uid)
u.setName("u")
u.setRole(UserRoleEnum.REGULAR)
}
})
}

private def buildEvent(eventType: RequestEvent.Type, sc: SecurityContext): RequestEvent = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,6 @@ class LiteLLMProxyAuthSpec extends AnyFlatSpec with Matchers with BeforeAndAfter
u.setUid(1)
u.setName("test")
u.setEmail("test@example.com")
u.setGoogleId(null)
u.setRole(role)
JwtAuth.jwtToken(JwtAuth.jwtClaims(u, expireInDays = 1))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import java.nio.charset.Charset
import javax.websocket.HandshakeResponse
import javax.websocket.server.{HandshakeRequest, ServerEndpointConfig}
import scala.jdk.CollectionConverters.{ListHasAsScala, _}
import scala.util.chaining.scalaUtilChainingOps

/**
* This configurator extracts user identity from the HTTP handshake request
Expand Down Expand Up @@ -64,22 +65,12 @@ class ServletAwareConfigurator extends ServerEndpointConfig.Configurator with La
s"User ID: $userId, User Name: $userName, User Email: $userEmail with CU Access: $cuAccess"
)

config.getUserProperties.put(
classOf[User].getName,
new User(
userId,
userName,
userEmail,
null,
null,
null,
null,
null,
null,
null,
null
)
)
val user = new User().tap { u =>
u.setUid(userId)
u.setName(userName)
u.setEmail(userEmail)
}
config.getUserProperties.put(classOf[User].getName, user)
logger.debug(s"User created from headers: ID=$userId, Name=$userName")
} else {
// SINGLE NODE MODE: Construct the User object from JWT in query parameters.
Expand All @@ -95,22 +86,12 @@ class ServletAwareConfigurator extends ServerEndpointConfig.Configurator with La
.get("access-token")
.map(token => {
val claims = jwtConsumer.process(token).getJwtClaims
config.getUserProperties.put(
classOf[User].getName,
new User(
claims.getClaimValue("userId").asInstanceOf[Long].toInt,
claims.getSubject,
String.valueOf(claims.getClaimValue("email").asInstanceOf[String]),
null,
null,
null,
null,
null,
null,
null,
null
)
)
val user = new User().tap { u =>
u.setUid(claims.getClaimValue("userId").asInstanceOf[Long].toInt)
u.setName(claims.getSubject)
u.setEmail(String.valueOf(claims.getClaimValue("email").asInstanceOf[String]))
}
config.getUserProperties.put(classOf[User].getName, user)
})
}
} catch {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ class TexeraWebApplication

environment.jersey.register(classOf[AuthResource])
environment.jersey.register(classOf[GoogleAuthResource])
environment.jersey.register(classOf[FacebookAuthResource])
environment.jersey.register(classOf[UserConfigResource])
environment.jersey.register(classOf[FeedbackResource])
environment.jersey.register(classOf[AdminUserResource])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,13 @@ import javax.ws.rs.core.SecurityContext
override protected def newInstance = new GuestAuthFilter
}

val GUEST: User =
new User(null, "guest", null, null, null, null, UserRoleEnum.REGULAR, null, null, null, null)
val GUEST: User = {
val user = new User()
user.setName("guest")
user.setRole(UserRoleEnum.REGULAR)
user
}

}

@PreMatching
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

package org.apache.texera.web.resource

import com.fasterxml.jackson.databind.ObjectMapper
import org.apache.texera.auth.JwtAuth.{TOKEN_EXPIRE_TIME_IN_MINUTES, jwtClaims, jwtToken}
import org.apache.texera.common.config.UserSystemConfig
import org.apache.texera.dao.jooq.generated.enums.ProviderTypeEnum
import org.apache.texera.web.model.http.response.TokenIssueResponse
import org.apache.texera.web.resource.auth.{ExternalAuthProvisioner, ExternalProfile}

import java.net.URI
import java.net.http.{HttpClient, HttpRequest, HttpResponse}
import javax.ws.rs.core.{MediaType, UriBuilder}
import javax.ws.rs._

@Path("/auth/facebook")
class FacebookAuthResource {

final private lazy val clientId = UserSystemConfig.facebookClientId

@GET
@Path("/clientid")
def getClientId: String = clientId

private val http = HttpClient.newHttpClient()
private val mapper = new ObjectMapper()

private def getJson(uri: URI) = {
val resp = http.send(
HttpRequest.newBuilder(uri).GET().build(),
HttpResponse.BodyHandlers.ofString()
)
if (resp.statusCode() != 200)
throw new NotAuthorizedException(s"Facebook API error: ${resp.statusCode()}")
mapper.readTree(resp.body())
}

private def verifyFacebookToken(accessToken: String): (String, Option[String], Option[String]) = {
val facebookUrl = "https://graph.facebook.com"
val appId = UserSystemConfig.facebookClientId
val appSecret = UserSystemConfig.facebookAppSecret
val appToken = s"$appId|$appSecret"

val verifyRequest = getJson(
UriBuilder
.fromUri(s"$facebookUrl/debug_token")
.queryParam("input_token", accessToken)
.queryParam("access_token", appToken)
.build()
).path("data")

if (
!verifyRequest
.path("is_valid")
.asBoolean(false) || verifyRequest.path("app_id").asText() != appId
)
throw new NotAuthorizedException("Invalid Facebook token")

val me = getJson(
UriBuilder
.fromUri(s"$facebookUrl/me")
.queryParam("fields", "id,name,email")
.queryParam("access_token", accessToken)
.build()
)

val id = me.path("id").asText()
val name = Option(me.path("name").asText(null))
val email = Option(me.path("email").asText(null))
(id, name, email)
}

@POST
@Consumes(Array(MediaType.TEXT_PLAIN))
@Produces(Array(MediaType.APPLICATION_JSON))
@Path("/login")
def login(credential: String): TokenIssueResponse = {
val (facebookId, nameOpt, emailOpt) = verifyFacebookToken(credential)

if (facebookId.isEmpty) throw new NotAuthorizedException("Login credentials are incorrect.")

val facebookEmail = emailOpt.filter(_.nonEmpty).getOrElse(s"$facebookId@facebook.local")
val facebookName = nameOpt.filter(_.nonEmpty).getOrElse(facebookEmail)

val user = ExternalAuthProvisioner.loginOrProvision(
ExternalProfile(ProviderTypeEnum.FACEBOOK, facebookId, facebookName, facebookEmail)
)

TokenIssueResponse(jwtToken(jwtClaims(user, TOKEN_EXPIRE_TIME_IN_MINUTES)))
}

}
Loading
Loading