Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[DNM] SAML Authentication #1593

Draft
wants to merge 7 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ gem 'omniauth', '>=1.2.2'
gem 'omniauth-facebook', '>=2.0.0'
gem 'omniauth-google-oauth2', '>=0.2.5'
gem 'omniauth-shibboleth', '>=1.1.2'
gem 'omniauth-saml', '>=1.7.0'
gem 'omniauth-rails_csrf_protection'
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be unnecessary with omniauth v2.0.0


# OAuth2 authentication
gem 'oauth2'
Expand Down
3 changes: 2 additions & 1 deletion app/controllers/application_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,8 @@ def verify_authenticity_token
" please contact the Autolab Development team at the " \
"contact link below"

authentication_failed(msg) unless verified_request?
# NOTE: commenting out the line below was the only way to get SAML auth to work
# authentication_failed(msg) unless verified_request?
end

def maintenance_mode?
Expand Down
62 changes: 62 additions & 0 deletions app/controllers/users/omniauth_callbacks_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -106,4 +106,66 @@ def shibboleth
end
end
end

def saml
if user_signed_in?
if data = request.env["omniauth.auth"]
# add this authentication object to current user
if current_user.authentications.where(provider: data["provider"],
uid: data["uid"]).empty?
current_user.authentications.create(provider: data["provider"],
uid: data["uid"])
end
end
redirect_to(root_path) && return
else
@user = User.find_for_saml(request.env["omniauth.auth"]["extra"]["raw_info"].attributes["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name"][0], current_user)
if @user # this user has signed into autolab before (or is on a class roster so autolab knows who they are)
sign_in_and_redirect @user, event: :authentication # this will throw if @user is not activated
set_flash_message(:notice, :success, kind: "SAML") if is_navigational_format?
else
# Skip sign up for a known user
data = request.env["omniauth.auth"]

@user = User.where(email: data["extra"]["raw_info"].attributes["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name"][0]).first # use the email as uid

# If user doesn't exist, create one first (school specific stuff follows)
if @user.nil?
@user = User.new
@user.email = data["extra"]["raw_info"].attributes["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name"][0] # it turns out this is where identity is
@user.first_name = data["extra"]["raw_info"].attributes["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname"][0]
@user.last_name = data["extra"]["raw_info"].attributes["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname"][0]

# Set user info based on LDAP lookup
if @user.email.include? "@andrew.cmu.edu"
ldapResult = User.ldap_lookup(@user.email.split("@")[0])
if ldapResult
@user.first_name = ldapResult[:first_name]
@user.last_name = ldapResult[:last_name]
@user.school = ldapResult[:school]
@user.major = ldapResult[:major]
@user.year = ldapResult[:year]
end
end

# If LDAP lookup failed, use (blank) as place holder
@user.first_name = "(blank)" if @user.first_name.nil?
@user.last_name = "(blank)" if @user.last_name.nil?

temp_pass = Devise.friendly_token[0, 20] # generate a random token
@user.password = temp_pass
@user.password_confirmation = temp_pass
@user.skip_confirmation!
end
# Rails.logger.error(data["uid"])
# Rails.logger.error( data["extra"]["raw_info"].attributes["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname"][0] )

@user.authentications.new(provider: "saml",
uid: data["extra"]["raw_info"].attributes["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name"][0])
@user.save!
sign_in_and_redirect @user, event: :authentication # this will throw if @user is not activated
set_flash_message(:notice, :success, kind: data["extra"]["raw_info"].attributes["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name"][0]) if is_navigational_format? #"SAML") if is_navigational_format?
end
end
end
end
13 changes: 12 additions & 1 deletion app/models/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ class User < ApplicationRecord
:recoverable, :rememberable, :trackable, :validatable,
:confirmable

devise :omniauthable, omniauth_providers: [:shibboleth]
devise :omniauthable, omniauth_providers: [:saml]

has_many :course_user_data, dependent: :destroy
has_many :courses, through: :course_user_data
Expand Down Expand Up @@ -97,6 +97,12 @@ def self.find_for_shibboleth_oauth(auth, _signed_in_resource = nil)
uid: auth.uid)
return authentication.user if authentication&.user
end
def self.find_for_saml(auth_uid, _signed_in_resource = nil)
authentication = Authentication.find_by(provider: "saml",
uid: auth_uid)
return authentication.user if authentication && authentication.user
end


def self.new_with_session(params, session)
super.tap do |user|
Expand All @@ -116,6 +122,11 @@ def self.new_with_session(params, session)
user.email = data["uid"] # email is uid in our case
user.authentications.new(provider: "CMU-Shibboleth",
uid: data["uid"])
elsif (data = session["devise.saml_data"])
user.email = data["uid"]
user.authentications.new(provider: "saml",
uid: data["uid"])

end
end
end
Expand Down
7 changes: 2 additions & 5 deletions app/views/devise/shared/_links.erb
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,12 @@
<%= link_to "Didn't receive unlock instructions?", new_unlock_path(resource_name) %><br />
<% end -%>

<% if false %>
<%- if devise_mapping.omniauthable? %>
<%- resource_class.omniauth_providers.each do |provider| %>
<% if provider.to_s.titleize == "Shibboleth" %>
<b><%= link_to "Sign in with your CMU account", user_omniauth_authorize_path(provider) %></b><br />
<b><%= button_to "Sign in with your CMU account", user_saml_omniauth_authorize_path %></b><br />
<% else %>
<%= link_to "Sign in with #{provider.to_s.titleize}", user_omniauth_authorize_path(provider) %><br />
<%= button_to "Sign in with #{provider.to_s.titleize}", user_saml_omniauth_authorize_path %><br />
<% end %>
<% end -%>
<% end %>

<% end -%>
8 changes: 8 additions & 0 deletions config/initializers/devise.rb
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,14 @@
# up on your models and hooks.
# config.omniauth :github, 'APP_ID', 'APP_SECRET', :scope => 'user,public_repo'

config.omniauth :saml, idp_cert_fingerprint: 'nnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnn', # insert your fingerprint here
idp_sso_target_url: 'https://login.microsoftonline.com/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/saml2', # replace with your url
issuer: 'https://autolab.myschool.edu',
assertion_consumer_service_url: 'https://autolab.myschool.edu/auth/users/auth/saml/callback',
idp_entity_id: 'https://sts.windows.net/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/', # similar to the target URL
name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent',
attribute_statements: { email: ['http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress']}

# ==> Warden configuration
# If you want to use other strategies, that are not supported by Devise, or
# change the failure app, you can configure them inside the config.warden block.
Expand Down
15 changes: 10 additions & 5 deletions docs/installation/docker-compose.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ First ensure that you have Docker and Docker Compose installed on your machine.
1. Clone this repository and its Autolab and Tango submodules:

:::bash
git clone --recurse-submodules -j8 git@github.com:autolab/docker.git autolab-docker
git clone --recurse-submodules -j8 https://github.com/autolab/docker.git autolab-docker


2. Enter the project directory:
Expand All @@ -23,6 +23,11 @@ First ensure that you have Docker and Docker Compose installed on your machine.
:::bash
make update

3a. If the previous step fails with `make: command not found`, you may need to install `make` with the appropriate command for your Linux system, such as:

:::bash
apt install make

4. Create initial default configs:

:::bash
Expand All @@ -31,12 +36,12 @@ First ensure that you have Docker and Docker Compose installed on your machine.
5. Build the Dockerfiles for both Autolab and Tango:

:::bash
docker-compose build
docker compose build

6. Run the Docker containers:

:::bash
docker-compose up -d
docker compose up -d

Note at this point Nginx will still be crash-looping in the Autolab container because TLS/SSL has not been configured/disabled yet.

Expand Down Expand Up @@ -98,7 +103,7 @@ There are three options for TLS: using Let's Encrypt (for free TLS certificates)
2. Ensure that port 443 is exposed on your server (i.e checking your firewall, AWS security group settings, etc)
3. In `ssl/init-letsencrypt.sh`, change `domains=(example.com)` to the list of domains that your host is associated with, and change `email` to be your email address so that Let's Encrypt will be able to email you when your certificate is about to expire
4. If necessary, change `staging=0` to `staging=1` to avoid being rate-limited by Let's Encrypt since there is a limit of 20 certificates/week. Setting this is helpful if you have an experimental setup.
5. Run your modified script: `sudo sh ./ssl/init-letsencrypt.sh`
5. Run your modified script: `su -c 'sh ./ssl/init-letsencrypt.sh'`

### Option 2: Using your own TLS certificate
1. Copy your private key to `ssl/privkey.pem`
Expand Down Expand Up @@ -304,4 +309,4 @@ You can resolve this by changing the owner of the files to be your current user,

This happens when you are accessing Autolab via localhost, as Tango will attempt to send the autograder logs to its own localhost instead.

To remedy this, add `127.0.0.1 autolab` to `/etc/hosts` and access Autolab via `http://autolab` instead of `http://localhost`.
To remedy this, add `127.0.0.1 autolab` to `/etc/hosts` and access Autolab via `http://autolab` instead of `http://localhost`.