Skip to content

Instantly share code, notes, and snippets.

@caendekerk
Created March 4, 2021 10:48
Show Gist options
  • Save caendekerk/f8291bf49ea197b3665ddd6d9befc31b to your computer and use it in GitHub Desktop.
Save caendekerk/f8291bf49ea197b3665ddd6d9befc31b to your computer and use it in GitHub Desktop.
Rails API only with Okta but without Devise
require_relative 'boot'
require 'rails'
# Pick the frameworks you want:
require 'active_model/railtie'
require 'active_job/railtie'
require 'active_record/railtie'
require 'action_controller/railtie'
require 'action_mailer/railtie'
require 'action_view/railtie'
# Require the gems listed in Gemfile, including any gems
# you've limited to :test, :development, or :production.
Bundler.require(*Rails.groups)
module APP_Api
class Application < Rails::Application
# Initialize configuration defaults for originally generated Rails version.
config.load_defaults 6.0
# Settings in config/environments/* take precedence over those specified here.
# Application configuration can go into files in config/initializers
# -- all .rb files in that directory are automatically loaded after loading
# the framework and any gems in your application.
# Only loads a smaller set of middleware suitable for API only apps.
# Middleware like session, flash, cookies can be added back manually.
# Skip views, helpers and assets when generating a new resource.
config.api_only = true
# https://github.com/omniauth/omniauth#integrating-omniauth-into-your-rails-api
config.session_store :cookie_store, key: '_APP_session', expire_after: 1.day
config.middleware.use ActionDispatch::Cookies
config.middleware.use ActionDispatch::Session::CookieStore, config.session_options
# Use :sql format instead of default :ruby in order to support PostgreSQL specific features
# such as views. See https://guides.rubyonrails.org/active_record_migrations.html#types-of-schema-dumps.
config.active_record.schema_format = :sql
end
end
class ApplicationController < ActionController::API
include ActionController::Caching
before_action :authenticate_user!
def authenticate_user!
jwt =JwtAuthService.decode(request_token)
user_id = jwt['user_id']
@current_user = User.find(user_id)
rescue JWT::VerificationError, JWT::DecodeError, ActiveRecord::RecordNotFound
raise Exception.new(status: 401)
end
# frozen_string_literal: true
class AuthenticationController < ApplicationController
include ActionController::RequestForgeryProtection
include ActionController::Cookies
include AbstractController::Rendering
include ActionView::Layouts
skip_before_action :authenticate_user!
# SECURITY: Must not be accessible across origins (from untrusted domains)
def csrf_token
render json: { csrfToken: form_authenticity_token }
end
def callback
user = User.create_or_initialize_by(email: request.env['omniauth.auth'].dig('info', 'email')&.downcase)
if user
session[:okta] = { user_id: user.id, expires: 30.seconds.since }
redirect_to '/login?submit=1'
else
raise Exception.new(status: 401, title: 'User is not authorized')
end
end
def create
user =
if session[:okta] && session[:okta].fetch(:expires, 0) > Time.zone.now
User.find_by(id: session[:okta][:user_id])
end
if user
render json: { jwt: JwtAuthService.encode({ user_id: user.id }) }
else
raise Exception.new(status: 401, title: 'No active session found')
end
end
private
def auth
request.env['omniauth.auth']
end
end
source 'https://rubygems.org'
# Bundle edge Rails instead: gem 'rails', github: 'rails/rails'
gem 'rails', '~> 6.0.0'
# Shorten boot time
gem 'bootsnap'
# Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder
gem 'jbuilder', github: 'rails/jbuilder'
gem 'oj'
# Authentication
gem 'jwt'
gem 'omniauth-okta', github: 'dandrews/omniauth-okta'
gem 'omniauth-rails_csrf_protection'
# PostgreSQL
gem 'pg'
require 'jwt'
def encode(payload)
payload = payload.dup
payload['exp'] = tomorrow.to_i
JWT.encode(payload, Rails.application.secrets.secret_key_base)
end
def decode(token)
decoded_token = JWT.decode(token, Rails.application.secrets.secret_key_base)
decoded_token.first
end
<div class="login"
[ngClass]="{'loading': isLoading}">
<form #oktaForm ngNoForm action="/auth/okta" method="POST" (submit)="isLoading=true">
<input type="hidden" name="authenticity_token" [value]="csrfToken">
<button class="login__button"
[disabled]="!csrfToken"
(click)="onSubmit(oktaForm)">
Login with Okta
</button>
</form>
</div>
class LoginPage {
csrfToken: string
isLoading = false
login() {
# Pseudocode
getCsrfToken(): Observable<{csrfToken: string}> {
return this.api.request('GET', '/auth/csrf_token')
}
}
return if (Rails.env.test? || Rails.env.e2e?)
require 'omniauth-okta'
Rails.application.config.middleware.use(
OmniAuth::Strategies::Okta,
ENV.fetch('OKTA_CLIENT_ID'),
ENV.fetch('OKTA_CLIENT_SECRET'),
scope: 'email groups',
fields: %w[email groups],
client_options: {
site: ENV.fetch('OKTA_ISSUER'),
authorize_url: ENV.fetch('OKTA_ISSUER') + '/v1/authorize',
token_url: ENV.fetch('OKTA_ISSUER') + '/v1/token',
user_info_url: ENV.fetch('OKTA_ISSUER') + '/v1/userinfo',
redirect_uri: ENV.fetch('OKTA_REDIRECT_URI')
}
)
get '/auth/csrf_token', to: 'authentication#csrf_token'
get '/auth/okta/callback', to: 'authentication#callback'
@kengreeff
Copy link

Very helpful! Thanks

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment