Skip to content

Commit 9f229cd

Browse files
authored
Merge pull request #989 from NDLANO/feat/feide-id-jwt-validation
Rewrite Feide auth to use ID tokens
2 parents 6636876 + 079ff53 commit 9f229cd

63 files changed

Lines changed: 1231 additions & 1318 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

article-api/src/main/scala/no/ndla/articleapi/ComponentRegistry.scala

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@ import no.ndla.common.Clock
3232
import no.ndla.common.util.TraitUtil
3333
import no.ndla.database.{DBMigrator, DBUtility, DataSource}
3434
import no.ndla.network.NdlaClient
35-
import no.ndla.network.clients.rediscache.FeideRedisClient
3635
import no.ndla.network.tapir.auth.{FeideAuth, NdlaAuth}
3736
import no.ndla.network.tapir.{
3837
ErrorHandling,
@@ -62,14 +61,13 @@ class ComponentRegistry(properties: ArticleApiProperties) extends TapirApplicati
6261
given traitUtil: TraitUtil = new TraitUtil
6362
given articleRepository: ArticleRepository = new ArticleRepository
6463
given converterService: ConverterService = new ConverterService
65-
given redisClient: FeideRedisClient = new FeideRedisClient(props.RedisHost, props.RedisPort)
6664
given ndlaClient: NdlaClient = new NdlaClient
6765
given searchApiClient: SearchApiClient = new SearchApiClient(props.SearchApiUrl)
6866
given feideApiClient: FeideApiClient = new FeideApiClient
6967
given myndlaApiClient: MyNDLAApiClient = new MyNDLAApiClient
7068
implicit val jwsKeySelectorFactory: JwsKeySelectorFactory = DefaultJwsKeySelectorFactory
71-
given ndlaAuth: NdlaAuth = NdlaAuth()
72-
given feideAuth: FeideAuth = FeideAuth()
69+
implicit lazy val ndlaAuth: NdlaAuth = NdlaAuth()
70+
implicit lazy val feideAuth: FeideAuth = FeideAuth()
7371
given frontpageApiClient: FrontpageApiClient = new FrontpageApiClient
7472
given imageApiClient: ImageApiClient = new ImageApiClient
7573
given taxonomyApiClient: TaxonomyApiClient = new TaxonomyApiClient(props.TaxonomyUrl)

article-api/src/main/scala/no/ndla/articleapi/service/ReadService.scala

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ import no.ndla.common.model.domain.article.Article
3131
import no.ndla.common.model.domain.{ArticleType, Availability}
3232
import no.ndla.database.DBUtility
3333
import no.ndla.language.Language
34-
import no.ndla.network.model.{FeideUserWrapper, userOrAccessDenied}
34+
import no.ndla.network.model.FeideUserWrapper
3535
import no.ndla.validation.HtmlTagRules.{jsoupDocumentToString, stringToJsoupDocument}
3636
import org.jsoup.nodes.Element
3737
import scalikejdbc.DBSession
@@ -72,7 +72,7 @@ class ReadService(using
7272
case Some(ArticleRow(_, _, _, _, _, Some(article))) if article.availability == Availability.everyone =>
7373
Cachable.yes(converterService.toApiArticleV2(article, language, fallback))
7474
case Some(ArticleRow(_, _, _, _, _, Some(article))) =>
75-
val feideUser = feide.flatMap(_.user)
75+
val feideUser = feide.map(_.user)
7676
val userIsTeacher = feideUser.exists(_.isTeacher)
7777
article.availability match {
7878
case Availability.teacher if !userIsTeacher =>
@@ -223,13 +223,7 @@ class ReadService(using
223223
shouldScroll: Boolean,
224224
feide: Option[FeideUserWrapper],
225225
): Try[Cachable[SearchResult[ArticleSummaryV2DTO]]] = {
226-
val availabilities = feide.map(_.userOrAccessDenied) match {
227-
case Some(Success(user)) => user.availabilities
228-
case None => Seq.empty
229-
case Some(Failure(_)) =>
230-
logger.info("User is not authenticated with Feide, assuming non-user")
231-
Seq.empty
232-
}
226+
val availabilities = feide.fold(Seq.empty)(_.user.availabilities)
233227

234228
val settings = query.emptySomeToNone match {
235229
case Some(q) => SearchSettings(
@@ -275,11 +269,10 @@ class ReadService(using
275269
else Cachable.yes(result)
276270
}
277271

278-
private def getAvailabilityFilter(feide: Option[FeideUserWrapper]): Option[Availability] =
279-
feide.userOrAccessDenied match {
280-
case Success(user) if user.isTeacher => None
281-
case _ => Some(Availability.everyone)
282-
}
272+
private def getAvailabilityFilter(feide: Option[FeideUserWrapper]): Option[Availability] = feide match {
273+
case Some(f) if f.user.isTeacher => None
274+
case _ => Some(Availability.everyone)
275+
}
283276

284277
private def applyAvailabilityFilter(feide: Option[FeideUserWrapper], articles: Seq[Article]): Seq[Article] = {
285278
val availabilityFilter = getAvailabilityFilter(feide)

article-api/src/test/scala/no/ndla/articleapi/TestEnvironment.scala

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ import no.ndla.common.Clock
1919
import no.ndla.common.util.TraitUtil
2020
import no.ndla.database.{DBMigrator, DBUtility, DataSource}
2121
import no.ndla.network.NdlaClient
22-
import no.ndla.network.clients.rediscache.FeideRedisClient
2322
import no.ndla.network.clients.{FeideApiClient, MyNDLAApiClient, SearchApiClient, TaxonomyApiClient}
2423
import no.ndla.network.tapir.*
2524
import no.ndla.scalatestsuite.DBUtilityStub
@@ -63,7 +62,6 @@ trait TestEnvironment extends MockitoSugar {
6362
implicit lazy val e4sClient: NdlaE4sClient = mock[NdlaE4sClient]
6463
implicit lazy val searchApiClient: SearchApiClient = mock[SearchApiClient]
6564
implicit lazy val feideApiClient: FeideApiClient = mock[FeideApiClient]
66-
implicit lazy val redisClient: FeideRedisClient = mock[FeideRedisClient]
6765
implicit lazy val frontpageApiClient: FrontpageApiClient = mock[FrontpageApiClient]
6866
implicit lazy val imageApiClient: ImageApiClient = mock[ImageApiClient]
6967
implicit lazy val taxonomyApiClient: TaxonomyApiClient = mock[TaxonomyApiClient]

article-api/src/test/scala/no/ndla/articleapi/service/ReadServiceTest.scala

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import no.ndla.common.model.api.{FrontPageDTO, MenuDTO}
2020
import no.ndla.common.model.domain.*
2121
import no.ndla.common.model.domain.myndla.MyNDLAUser
2222
import no.ndla.network.clients.FeideExtendedUserInfo
23-
import no.ndla.network.model.FeideUserWrapper
23+
import no.ndla.network.model.{FeideIdToken, FeideUserWrapper}
2424
import org.mockito.ArgumentMatchers.{eq as eqTo, *}
2525
import org.mockito.Mockito.*
2626

@@ -165,7 +165,6 @@ class ReadServiceTest extends UnitSuite with TestEnvironment {
165165
}
166166

167167
test("that getArticlesByIds doesn't perform filter when every article has availability status everyone") {
168-
val feideId = "asd"
169168
val ids = List(1L, 2L, 3L)
170169
val article1 = TestData.sampleDomainArticle.copy(id = Some(1), availability = Availability.everyone)
171170
val article2 = TestData.sampleDomainArticle.copy(id = Some(2), availability = Availability.everyone)
@@ -179,8 +178,6 @@ class ReadServiceTest extends UnitSuite with TestEnvironment {
179178
.getArticlesByIds(articleIds = ids, language = "nb", fallback = true, page = 1, pageSize = 10, feide = None)
180179
.get
181180
result.length should be(3)
182-
183-
verify(feideApiClient, times(0)).getFeideExtendedUser(Some(feideId))
184181
}
185182

186183
test("that getArticlesByIds performs filter and returns articles that can only be seen by teacher") {
@@ -197,7 +194,7 @@ class ReadServiceTest extends UnitSuite with TestEnvironment {
197194

198195
val userMock = mock[MyNDLAUser]
199196
when(userMock.isTeacher).thenReturn(true)
200-
val feideUserInfo = FeideUserWrapper("test-token", Some(userMock))
197+
val feideUserInfo = FeideUserWrapper(userMock, mock[FeideIdToken])
201198

202199
val result = readService
203200
.getArticlesByIds(
@@ -220,7 +217,7 @@ class ReadServiceTest extends UnitSuite with TestEnvironment {
220217
val article3 = TestData.sampleDomainArticle.copy(id = Some(3), availability = Availability.teacher)
221218
val userMock = mock[MyNDLAUser]
222219
when(userMock.isTeacher).thenReturn(false)
223-
val feideUserInfo = FeideUserWrapper("test-token", Some(userMock))
220+
val feideUserInfo = FeideUserWrapper(userMock, mock[FeideIdToken])
224221

225222
when(articleRepository.withIds(any, any, any)(using any)).thenReturn(
226223
Success(Seq(toArticleRow(article1), toArticleRow(article2), toArticleRow(article3)))
@@ -241,7 +238,6 @@ class ReadServiceTest extends UnitSuite with TestEnvironment {
241238
}
242239

243240
test("that getArticlesByIds performs filter if feideAccessToken is not set") {
244-
val feideId = "asd"
245241
val ids = List(1L, 2L, 3L)
246242
val article1 = TestData.sampleDomainArticle.copy(id = Some(1), availability = Availability.everyone)
247243
val article2 = TestData.sampleDomainArticle.copy(id = Some(2), availability = Availability.everyone)
@@ -257,8 +253,6 @@ class ReadServiceTest extends UnitSuite with TestEnvironment {
257253
.get
258254
result.length should be(2)
259255
result.map(res => res.availability).contains("teacher") should be(false)
260-
261-
verify(feideApiClient, times(0)).getFeideAccessTokenOrFail(Some(feideId))
262256
}
263257

264258
test("that getArticlesByIds fails if no ids were given") {

common/src/main/scala/no/ndla/common/configuration/BaseProps.scala

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,11 @@ trait BaseProps extends StrictLogging {
150150
val ndlaAuth0LegacyIssuer = s"https://$ndlaAuth0LegacyHost/"
151151
val ndlaAuth0Audience = "ndla_system"
152152

153+
val feideIssuer: String = "https://auth.dataporten.no"
154+
val feideAuthorizationUrl: String = s"$feideIssuer/oauth/authorization"
155+
val feideTokenUrl: String = s"$feideIssuer/oauth/token"
156+
def feideClientId: Option[String] = propOrNone("FEIDE_CLIENT_ID")
157+
153158
def MAX_SEARCH_THREADS: Int = intPropOrDefault("MAX_SEARCH_THREADS", 100)
154159
def SEARCH_INDEX_SHARDS: Int = intPropOrDefault("SEARCH_INDEX_SHARDS", 1)
155160
def SEARCH_INDEX_REPLICAS: Int = intPropOrDefault("SEARCH_INDEX_REPLICAS", 1)

image-api/src/main/scala/no/ndla/imageapi/ComponentRegistry.scala

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ import no.ndla.imageapi.service.*
2323
import no.ndla.imageapi.service.search.*
2424
import no.ndla.network.NdlaClient
2525
import no.ndla.network.clients.MyNDLAApiClient
26-
import no.ndla.network.clients.rediscache.FeideRedisClient
2726
import no.ndla.network.jwt.{DefaultJwsKeySelectorFactory, JwsKeySelectorFactory}
2827
import no.ndla.network.tapir.auth.NdlaAuth
2928
import no.ndla.network.tapir.*
@@ -38,7 +37,6 @@ class ComponentRegistry(properties: ImageApiProperties) extends TapirApplication
3837

3938
implicit lazy val s3Client: NdlaS3Client = new NdlaS3Client(props.StorageName, props.StorageRegion)
4039
implicit lazy val cloudFrontClient: NdlaCloudFrontClient = new NdlaCloudFrontClient
41-
given redisClient: FeideRedisClient = new FeideRedisClient(props.RedisHost, props.RedisPort)
4240
given ndlaClient: NdlaClient = new NdlaClient
4341
implicit lazy val e4sClient: NdlaE4sClient = Elastic4sClientFactory.getClient(props.SearchServer)
4442
given searchLanguage: SearchLanguage = new SearchLanguage
@@ -47,7 +45,7 @@ class ComponentRegistry(properties: ImageApiProperties) extends TapirApplication
4745
given converterService: ConverterService = new ConverterService
4846
implicit lazy val myndlaApiClient: MyNDLAApiClient = new MyNDLAApiClient
4947
implicit val jwsKeySelectorFactory: JwsKeySelectorFactory = DefaultJwsKeySelectorFactory
50-
given ndlaAuth: NdlaAuth = NdlaAuth()
48+
implicit lazy val ndlaAuth: NdlaAuth = NdlaAuth()
5149
given searchConverterService: SearchConverterService = new SearchConverterService
5250
given dbUtility: DBUtility = new DBUtility
5351
given dbImageMetaInformation: DBImageMetaInformation = new DBImageMetaInformation
@@ -57,7 +55,7 @@ class ComponentRegistry(properties: ImageApiProperties) extends TapirApplication
5755
implicit lazy val tagIndexService: TagIndexService = new TagIndexService
5856
implicit lazy val tagSearchService: TagSearchService = new TagSearchService
5957
given validationService: ValidationService = new ValidationService
60-
given bulkUploadStore: BulkUploadStore = new BulkUploadStore
58+
given bulkUploadStore: BulkUploadStore = BulkUploadStore(props.RedisHost, props.RedisPort)
6159
given readService: ReadService = new ReadService
6260
implicit lazy val imageStorage: ImageStorageService = new ImageStorageService
6361
given writeService: WriteService = new WriteService

image-api/src/main/scala/no/ndla/imageapi/service/BulkUploadStore.scala

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ package no.ndla.imageapi.service
1111
import com.typesafe.scalalogging.StrictLogging
1212
import no.ndla.common.CirceUtil
1313
import no.ndla.imageapi.model.api.bulk.BulkUploadStateDTO
14-
import no.ndla.network.clients.rediscache.{FeideRedisClient, RedisStoredType}
14+
import no.ndla.network.clients.rediscache.{RedisStoredType, ScalaJedis}
1515

1616
import java.util.UUID
1717
import scala.concurrent.duration.{Duration, DurationInt}
@@ -24,9 +24,7 @@ object BulkUploadType extends RedisStoredType {
2424
val stateField: String = "state"
2525
}
2626

27-
class BulkUploadStore(using redisClient: FeideRedisClient) extends StrictLogging {
28-
private val jedis = redisClient.jedis
29-
27+
class BulkUploadStore(jedis: ScalaJedis) extends StrictLogging {
3028
def get(uploadId: UUID): Try[Option[BulkUploadStateDTO]] = jedis
3129
.hget(BulkUploadType, uploadId.toString, BulkUploadType.stateField)
3230
.flatMap {
@@ -37,9 +35,13 @@ class BulkUploadStore(using redisClient: FeideRedisClient) extends StrictLogging
3735
def set(uploadId: UUID, state: BulkUploadStateDTO): Try[Unit] = {
3836
val json = CirceUtil.toJsonString(state)
3937
for {
40-
newTtl <- jedis.getNewTTL(BulkUploadType, uploadId.toString)
38+
newTtl <- jedis.getFieldNewTtl(BulkUploadType, uploadId.toString, BulkUploadType.stateField)
4139
_ <- jedis.hset(BulkUploadType, uploadId.toString, BulkUploadType.stateField, json)
42-
_ <- jedis.expire(BulkUploadType, uploadId.toString, newTtl)
40+
_ <- jedis.hexpire(BulkUploadType, uploadId.toString, BulkUploadType.stateField, newTtl)
4341
} yield ()
4442
}
4543
}
44+
45+
object BulkUploadStore {
46+
def apply(host: String, port: Int): BulkUploadStore = new BulkUploadStore(ScalaJedis(host, port))
47+
}

image-api/src/test/scala/no/ndla/imageapi/e2e/BulkUploadTest.scala

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,10 @@ import no.ndla.imageapi.service.ImageStorageService
2424
import no.ndla.imageapi.service.search.{ImageIndexService, ImageSearchService, TagIndexService, TagSearchService}
2525
import no.ndla.imageapi.{ComponentRegistry, ImageApiProperties, MainClass, UnitSuite}
2626
import no.ndla.network.clients.MyNDLAApiClient
27-
import no.ndla.network.jwt.JwsKeySelectorFactory
27+
import no.ndla.network.tapir.auth.NdlaAuth
2828
import no.ndla.scalatestsuite.{DatabaseIntegrationSuite, RedisIntegrationSuite}
2929
import no.ndla.search.NdlaE4sClient
30-
import no.ndla.tapirtesting.{NdlaAuthTestTokens, TestJwsKeySelectorFactory}
30+
import no.ndla.tapirtesting.{NdlaAuthTest, NdlaAuthTestTokens}
3131
import org.mockito.ArgumentMatchers.any
3232
import org.mockito.Mockito.{when, withSettings}
3333
import org.mockito.quality.Strictness
@@ -73,7 +73,7 @@ class BulkUploadTest extends DatabaseIntegrationSuite with RedisIntegrationSuite
7373
mock[NdlaCloudFrontClient](withSettings.strictness(Strictness.LENIENT))
7474
override implicit lazy val myndlaApiClient: MyNDLAApiClient =
7575
mock[MyNDLAApiClient](withSettings.strictness(Strictness.LENIENT))
76-
override implicit val jwsKeySelectorFactory: JwsKeySelectorFactory = TestJwsKeySelectorFactory
76+
override implicit lazy val ndlaAuth: NdlaAuth = NdlaAuthTest()
7777

7878
override implicit lazy val imageStorage: ImageStorageService = {
7979
val m = mock[ImageStorageService](withSettings.strictness(Strictness.LENIENT))

image-api/src/test/scala/no/ndla/imageapi/service/BulkUploadStoreTest.scala

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ package no.ndla.imageapi.service
1111
import no.ndla.common.CirceUtil
1212
import no.ndla.imageapi.{TestEnvironment, UnitSuite}
1313
import no.ndla.imageapi.model.api.bulk.{BulkUploadItemDTO, BulkUploadItemStatus, BulkUploadStateDTO, BulkUploadStatus}
14-
import no.ndla.network.clients.rediscache.{FeideRedisClient, RedisStoredType, ScalaJedis}
14+
import no.ndla.network.clients.rediscache.{RedisStoredType, ScalaJedis}
1515
import org.mockito.ArgumentMatchers.{any, eq as eqTo}
1616
import org.mockito.Mockito.{reset, verify, when}
1717

@@ -20,13 +20,8 @@ import java.util.concurrent.atomic.AtomicReference
2020
import scala.util.{Failure, Success}
2121

2222
class BulkUploadStoreTest extends UnitSuite with TestEnvironment {
23-
24-
given redisClient: FeideRedisClient = mock[FeideRedisClient]
25-
val jedis: ScalaJedis = mock[ScalaJedis]
26-
when(redisClient.jedis).thenReturn(jedis)
27-
28-
override implicit lazy val bulkUploadStore: BulkUploadStore = new BulkUploadStore
29-
private val store: BulkUploadStore = bulkUploadStore
23+
private val jedis: ScalaJedis = mock[ScalaJedis]
24+
private val store: BulkUploadStore = new BulkUploadStore(jedis)
3025

3126
override def beforeEach(): Unit = reset(jedis)
3227

@@ -73,10 +68,13 @@ class BulkUploadStoreTest extends UnitSuite with TestEnvironment {
7368
test("set writes the encoded state and refreshes the TTL using BulkUploadType.cacheTime") {
7469
val uploadId = UUID.randomUUID()
7570
val expectedTtl = BulkUploadType.cacheTime.toSeconds
76-
when(jedis.getNewTTL(eqTo(BulkUploadType), eqTo(uploadId.toString))).thenReturn(Success(expectedTtl))
71+
when(jedis.getFieldNewTtl(eqTo(BulkUploadType), eqTo(uploadId.toString), eqTo(BulkUploadType.stateField)))
72+
.thenReturn(Success(expectedTtl))
7773
when(jedis.hset(eqTo(BulkUploadType), eqTo(uploadId.toString), eqTo(BulkUploadType.stateField), any[String]))
7874
.thenReturn(Success(1L))
79-
when(jedis.expire(eqTo(BulkUploadType), eqTo(uploadId.toString), eqTo(expectedTtl))).thenReturn(Success(1L))
75+
when(
76+
jedis.hexpire(eqTo(BulkUploadType), eqTo(uploadId.toString), eqTo(BulkUploadType.stateField), eqTo(expectedTtl))
77+
).thenReturn(Success(1L))
8078

8179
store.set(uploadId, sampleState) should be(Success(()))
8280

@@ -86,19 +84,27 @@ class BulkUploadStoreTest extends UnitSuite with TestEnvironment {
8684
eqTo(BulkUploadType.stateField),
8785
eqTo(CirceUtil.toJsonString(sampleState)),
8886
)
89-
verify(jedis).expire(eqTo(BulkUploadType), eqTo(uploadId.toString), eqTo(expectedTtl))
87+
verify(jedis).hexpire(
88+
eqTo(BulkUploadType),
89+
eqTo(uploadId.toString),
90+
eqTo(BulkUploadType.stateField),
91+
eqTo(expectedTtl),
92+
)
9093
}
9194

9295
test("set followed by get round-trips a state through the cache without losing fields") {
9396
val uploadId = UUID.randomUUID()
9497
val capturedJson = new AtomicReference[String]()
95-
when(jedis.getNewTTL(eqTo(BulkUploadType), eqTo(uploadId.toString))).thenReturn(Success(1L))
98+
when(jedis.getFieldNewTtl(eqTo(BulkUploadType), eqTo(uploadId.toString), eqTo(BulkUploadType.stateField)))
99+
.thenReturn(Success(1L))
96100
when(jedis.hset(eqTo(BulkUploadType), eqTo(uploadId.toString), eqTo(BulkUploadType.stateField), any[String]))
97101
.thenAnswer { i =>
98102
capturedJson.set(i.getArgument[String](3))
99103
Success(1L)
100104
}
101-
when(jedis.expire(any[RedisStoredType], any[String], any[Long])).thenReturn(Success(1L))
105+
when(jedis.hexpire(any[RedisStoredType], any[String], eqTo(BulkUploadType.stateField), any[Long])).thenReturn(
106+
Success(1L)
107+
)
102108
when(jedis.hget(eqTo(BulkUploadType), eqTo(uploadId.toString), eqTo(BulkUploadType.stateField))).thenAnswer(_ =>
103109
Success(Option(capturedJson.get()))
104110
)

0 commit comments

Comments
 (0)